Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular 19 - Gen2 SDK Sluggish User Interaction #3904

Open
jakehockey10 opened this issue Feb 11, 2025 · 6 comments
Open

Angular 19 - Gen2 SDK Sluggish User Interaction #3904

jakehockey10 opened this issue Feb 11, 2025 · 6 comments

Comments

@jakehockey10
Copy link
Contributor

jakehockey10 commented Feb 11, 2025

Describe the bug
In the process of upgrading my builder.io usage to use the Gen2 SDK, I updated my components to use the BuilderContent, replacing the BuilderComponent. I'm providing my apiKey, my model ("page"), and my content (using fetchOneEntry). No matter which one of my builder pages I visit, the user interaction becomes extremely sluggish. No changes were made to the content, and the Gen1 implementation does not have this problem. Pages with sidebars will be so sluggish that upon clicking the button to open the sidebar, several seconds go by before anything happens visually, and the animations are not rendered. The sidebar does not close any more with the use of Gen2.

I'm having trouble diagnosing what the issue is. I'm attempting to use DevTools to determine what code is executing to make the app feel so sluggish, but memory seems fine, and I don't get much information from the profiler that looks like lots of executions. If I click the pause button on the debugger, I don't break execution until some 30 second ping message on a websocket goes off, making me think the app is fairly idol.

And when I go to pages that don't have any builder content, I see no performance issues whatsoever. I'm just not sure how to further diagnose the issue. But all user interaction of any kind on builder content is delayed by at least several seconds if not longer. Hover over clickable elements takes seconds to show a different cursor. But I have to point out that this happens with pages that contain purely just sections of text as well, with no intractable elements. Even just a privacy policy page is causing extreme stuttering while attempting to scroll through the content. But this is only the builder.io content. On pages without builder.io content, scrolling is as smooth as would be expected.

I intend only to provide a data point as I acknowledge that Gen2 is in beta.

To Reproduce
Steps to reproduce the behavior:

  1. Start with Angular 19 app using Gen1 SDK
  2. Upgrade to using the Gen2 SDK

Expected behavior
I expect there to be no degradation in performance.

Screenshots
If applicable, add screenshots to help explain your problem.

Additional context
Below are examples of how I'm using the Gen2 library:

import {
  ChangeDetectionStrategy,
  Component,
  inject,
  resource,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Content, fetchOneEntry } from '@builder.io/sdk-angular';
import { environment } from '../../environments/environment';
import { EmailSubscriptionService } from '../email-subscription.service';

@Component({
  selector: 'app-cms-page',
  imports: [MatProgressSpinnerModule, Content],
  templateUrl: './cms-page.component.html',
  styleUrl: './cms-page.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CmsPageComponent {
  readonly #emailSubscriptions = inject(EmailSubscriptionService);

  protected readonly context = toSignal(this.#emailSubscriptions.context$);

  protected readonly apiKey = environment.builder.apiKey;

  protected readonly content = resource({
    loader: () =>
      fetchOneEntry({
        apiKey: this.apiKey,
        model: 'page',
        userAttributes: { urlPath: window.location.pathname || '' },
      }),
  });
}
@if (content.isLoading()) {
  <mat-spinner />
} @else if (content.value()) {
  <builder-content
    model="page"
    [content]="content.value()"
    [apiKey]="apiKey"
    [context]="context()"
  />
} @else if (content.error()) {
  <div>{{ content.error() }}</div>
} @else {
  <div>404 - Content not found</div>
}
import {
  ChangeDetectionStrategy,
  Component,
  OnChanges,
  resource,
  SimpleChanges,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouterOutlet } from '@angular/router';
import { Content, fetchOneEntry } from '@builder.io/sdk-angular';
import { derivedFrom } from 'ngxtension/derived-from';
import { injectNavigationEnd } from 'ngxtension/navigation-end';
import { pipe } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, Content],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  readonly #isLaunchPage = toSignal(
    injectNavigationEnd().pipe(map((event) => event.url === '/'))
  );

  protected readonly apiKey = environment.builder.apiKey;
  protected readonly headerContent = resource({
    loader: () => fetchOneEntry({ apiKey: this.apiKey, model: 'header' }),
  });
  protected readonly footerContent = resource({
    loader: () => fetchOneEntry({ apiKey: this.apiKey, model: 'footer' }),
  });
  protected readonly showHeader = derivedFrom(
    [this.#isLaunchPage, this.headerContent.value],
    pipe(map(([isLaunchPage, value]) => !isLaunchPage && value)),
    {
      initialValue: false,
    }
  );
  protected readonly showFooter = derivedFrom(
    [this.#isLaunchPage, this.footerContent.value],
    pipe(map(([isLaunchPage, value]) => !isLaunchPage && value)),
    {
      initialValue: false,
    }
  );
}
@if (showHeader()) {
  <header>
    <builder-content
      model="header"
      [apiKey]="apiKey"
      [content]="headerContent.value()"
    />
  </header>
}

<main>
  <router-outlet />
</main>

@if (showFooter()) {
  <footer>
    <builder-content
      model="footer"
      [apiKey]="apiKey"
      [content]="footerContent.value()"
    />
  </footer>
}
@jakehockey10 jakehockey10 changed the title Angular 19 - Gen2 SDK Angular 19 - Gen2 SDK Sluggish User Interaction Feb 11, 2025
@samijaber
Copy link
Contributor

samijaber commented Feb 11, 2025

Hi @jakehockey10, thanks for sharing this information, it's very useful for us to know.

In general, I recommend that you share such feedback via the Builder support channels, as this will guarantee that we associate your feedback with your organization/space, which means we can link your content with your bug report for easier debugging. It also means we can let you know when improvements are eventually made.

Would you be able to provide an example of a content URL (i.e. https://builder.io/content/:id) of a content that highlights this problem?

@jakehockey10
Copy link
Contributor Author

@samijaber thanks for the response. I can reach out to Builder Support instead on this. I was just very much interested in diagnosing this myself as I'm interested in how Builder works and figured it would be a worthwhile experience to converse with the development team on this one. But I also understand that that takes up time if the team has the patience to deal with me ;-)

Here is the privacy page that is just one example of the issue: builder.io/content/f1b26c07260c4947871a6e2639e92329.

I do want to re-iterate that I'm guessing this isn't an issue with the content itself. A version of the app that is still using Gen1 has no performance issues whatsoever and all animations/scrolling are smooth as can be. It is just the Gen2 SDK (I've only run it local so far, but I also created a build locally and ran http-server to see if that made a difference and it did not) that this behavior surfaces.

Please let me know if you'd like me to go through Builder Support on this one or if I can provide any additional information.

@jakehockey10
Copy link
Contributor Author

@samijaber as another data point:

I followed the following steps:

  • ng new BuilderTest
  • ng g c cms-page
  • ng add @angular/material
  • ng add ngxtension
  • ng g @angular/material:navigation

I made a few small tweaks after these commands were run, and my code is displayed below. But what I noticed is that, before adding any other dependencies besides Builder, the performance was much better. Then I added Angular Material and Ngxtension. The performance didn't get much worse, but when I started rendering the builder content inside the navigation component, that's when I noticed the performance getting worse again.

This makes me think this is a css problem with the way the positioning I have for the builder content vs. its parents are maybe causing something?

But I tested it with several different pages in my builder content, and the ones with just text were having the sluggish scrolling for about 10 seconds before it calmed down. The other pages that have sidebars and/or expanding panels, those ones showed the biggest degradation in performance from my perspective. The panels took several seconds to expand, the sidebar took several seconds to expand, and the animations were not smooth.

I've also tested this new blank project on a different computer than yesterday. I still can't seem to determine what is slowing it down, but I think I have a reproduceable case here.

Routes

import { Routes } from '@angular/router';
import { CmsPageComponent } from './cms-page/cms-page.component';

export const routes: Routes = [{ path: '**', component: CmsPageComponent }];

Navigation

import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterOutlet } from '@angular/router';
import { map, shareReplay } from 'rxjs/operators';

@Component({
  selector: 'app-navigation',
  templateUrl: './navigation.component.html',
  styleUrl: './navigation.component.scss',
  standalone: true,
  imports: [
    MatToolbarModule,
    MatButtonModule,
    MatSidenavModule,
    MatListModule,
    MatIconModule,
    RouterOutlet,
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NavigationComponent {
  private readonly breakpointObserver = inject(BreakpointObserver);

  protected readonly isHandset = toSignal(
    this.breakpointObserver.observe(Breakpoints.Handset).pipe(
      map((result) => result.matches),
      shareReplay()
    )
  );
}
<mat-sidenav-container class="sidenav-container">
  <mat-sidenav
    #drawer
    class="sidenav"
    fixedInViewport
    [attr.role]="isHandset() ? 'dialog' : 'navigation'"
    [mode]="isHandset() ? 'over' : 'side'"
    [opened]="isHandset() === false"
  >
    <mat-toolbar>Menu</mat-toolbar>
    <mat-nav-list>
      <a mat-list-item href="#">Link 1</a>
      <a mat-list-item href="#">Link 2</a>
      <a mat-list-item href="#">Link 3</a>
    </mat-nav-list>
  </mat-sidenav>
  <mat-sidenav-content>
    <mat-toolbar color="primary">
      @if (isHandset()) {
      <button
        type="button"
        aria-label="Toggle sidenav"
        mat-icon-button
        (click)="drawer.toggle()"
      >
        <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
      </button>
      }
      <span>BuilderTest</span>
    </mat-toolbar>
    <main><router-outlet /></main>
  </mat-sidenav-content>
</mat-sidenav-container>

CmsPageComponent

import { ChangeDetectionStrategy, Component, resource } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Content, fetchOneEntry } from '@builder.io/sdk-angular';
import { injectNavigationEnd } from 'ngxtension/navigation-end';
import { map } from 'rxjs';

@Component({
  selector: 'app-cms-page',
  imports: [Content],
  templateUrl: './cms-page.component.html',
  styleUrl: './cms-page.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CmsPageComponent {
  protected readonly content = resource({
    loader: () =>
      fetchOneEntry({
        apiKey: 'my-api-key',
        model: 'page',
        userAttributes: { urlPath: window.location.pathname || '' },
      }),
  });

  protected readonly context = toSignal(
    injectNavigationEnd().pipe(
      map((event) =>
        event.url === '/'
          ? { submitForm: () => console.log('fake submission') }
          : undefined
      )
    )
  );
}
<builder-content
  [apiKey]="'my-api-key'"
  [content]="content.value()"
  [context]="context()"
/>

AppComponent

<app-navigation />

@jakehockey10
Copy link
Contributor Author

jakehockey10 commented Feb 11, 2025

@samijaber I recorded/profiled interacting with a page (using Angular DevTools extension) in this demo app and just scrolled and expanded some accordion sections. Then I exported the profile. Would this file be helpful to look at? The barchart shows that 809 ms were spent on a _Block (98 instances) and it looks to all be related to ngOnChanges.

NgDevTools-Profile-2025-02-11T23_36_12.json

What's weird, is I also noticed in this demo app that if I am using the scroll wheel, the performance issue is much more noticeable. However, if you can get the scroll bar under your mouse cursor and drag it up and down, after an initial delay, the scroll is magically smooth again.

Somehow, builder-content is interacting with the cdk scrollable in a negative way? It sure seems change detection related. For instance, in this demo application, my app.config.ts file looked like this:

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideAnimationsAsync(),
  ],
};

If I set eventCoalescing to false, the problem is much worse. If I slowly but continuously scroll from the top of the page to the bottom, I don't visually see a change until I'm down at the bottom, done scrolling. So change detection is firing like crazy, even for this app that I'm not doing much of anything other than rendering Builder content.

@samijaber
Copy link
Contributor

@jakehockey10 Thanks a lot for the detailed feedback. We are going to take a look and see what can be done, and update you accordingly.

@jakehockey10
Copy link
Contributor Author

@samijaber I took another stab at trying to diagnose the issue, and I put some breakpoints in the ngOnChanges lifecycle hooks of the various components in the builder js, and it seems like some input values are showing up as changes because they are objects and the previousValue and currentValue are different, referentially. They have the same primitive values in each of their properties, but are none-the-less showing up as changes in ngOnChanges and it seems potentially significant work is being done to update attributes and such unnecessarily, on each invocation. In some cases, just on simple hover over any of the content coming from Builder, I can get myself in an endless loop of hitting those ngOnChanges breakpoints over and over, with the same object inputs showing as changed, but with their properties holding the same values as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants