From df6bf95f35a315f21d2aa91cff18bd4a09d27d2f Mon Sep 17 00:00:00 2001 From: Jon Rimmer Date: Thu, 14 Mar 2019 18:36:19 +0000 Subject: [PATCH] Fix performance of Angular rerender --- addons/knobs/src/registerKnobs.js | 18 +++++++--- app/angular/package.json | 1 + .../angular/components/app.component.ts | 33 +++++++++++++------ .../src/client/preview/angular/helpers.ts | 15 +++++++-- app/angular/src/client/preview/render.js | 4 +-- .../src/stories/addon-knobs.stories.ts | 2 +- examples/angular-cli/tsconfig.json | 2 ++ tslint.json | 1 - 8 files changed, 55 insertions(+), 21 deletions(-) diff --git a/addons/knobs/src/registerKnobs.js b/addons/knobs/src/registerKnobs.js index e6b81396d148..16eb9356b458 100644 --- a/addons/knobs/src/registerKnobs.js +++ b/addons/knobs/src/registerKnobs.js @@ -18,18 +18,28 @@ function setPaneKnobs(timestamp = +new Date()) { channel.emit(SET, { knobs: knobStore.getAll(), timestamp }); } -// Increased performance by reducing the number of times a component is rendered during knob changes -const debouncedOnKnobChanged = debounce(() => { +const resetAndForceUpdate = () => { knobStore.markAllUnused(); forceReRender(); -}, COMPONENT_FORCE_RENDER_DEBOUNCE_DELAY_MS); +}; + +// Increase performance by reducing how frequently the story is recreated during knob changes +const debouncedResetAndForceUpdate = debounce( + resetAndForceUpdate, + COMPONENT_FORCE_RENDER_DEBOUNCE_DELAY_MS +); function knobChanged(change) { const { name } = change; const { value } = change; // Update the related knob and it's value. const knobOptions = knobStore.get(name); knobOptions.value = value; - debouncedOnKnobChanged(); + + if (!manager.options.disableDebounce) { + debouncedResetAndForceUpdate(); + } else { + resetAndForceUpdate(); + } } function knobClicked(clicked) { diff --git a/app/angular/package.json b/app/angular/package.json index 29999b7b5332..8604c380cdf1 100644 --- a/app/angular/package.json +++ b/app/angular/package.json @@ -51,6 +51,7 @@ "@angular/platform-browser-dynamic": ">=6.0.0", "autoprefixer": "^8.1.0", "babel-loader": "^7.0.0 || ^8.0.0", + "rxjs": "^6.0.0", "zone.js": "^0.8.29" }, "publishConfig": { diff --git a/app/angular/src/client/preview/angular/components/app.component.ts b/app/angular/src/client/preview/angular/components/app.component.ts index 264b6e90722d..1bc67b7f4015 100644 --- a/app/angular/src/client/preview/angular/components/app.component.ts +++ b/app/angular/src/client/preview/angular/components/app.component.ts @@ -16,6 +16,8 @@ import { } from '@angular/core'; import { STORY } from '../app.token'; import { NgStory, ICollection } from '../types'; +import { Observable, Subscription } from 'rxjs'; +import { first } from 'rxjs/operators'; @Component({ selector: 'storybook-dynamic-app-root', @@ -24,22 +26,33 @@ import { NgStory, ICollection } from '../types'; export class AppComponent implements OnInit, OnDestroy { @ViewChild('target', { read: ViewContainerRef }) target: ViewContainerRef; - constructor(private cfr: ComponentFactoryResolver, @Inject(STORY) private data: NgStory) {} + + subscription: Subscription; + + constructor( + private cfr: ComponentFactoryResolver, + @Inject(STORY) private data: Observable + ) {} ngOnInit(): void { - this.putInMyHtml(); - } + this.data.pipe(first()).subscribe((data: NgStory) => { + this.target.clear(); + const compFactory = this.cfr.resolveComponentFactory(data.component); + const ref = this.target.createComponent(compFactory); + const instance = ref.instance; - ngOnDestroy(): void { - this.target.clear(); + this.subscription = this.data.subscribe(newData => { + this.setProps(instance, newData); + ref.changeDetectorRef.detectChanges(); + }); + }); } - private putInMyHtml(): void { + ngOnDestroy(): void { this.target.clear(); - const compFactory = this.cfr.resolveComponentFactory(this.data.component); - const instance = this.target.createComponent(compFactory).instance; - - this.setProps(instance, this.data); + if (this.subscription) { + this.subscription.unsubscribe(); + } } /** diff --git a/app/angular/src/client/preview/angular/helpers.ts b/app/angular/src/client/preview/angular/helpers.ts index 8cabe40c1aa3..d85fa451edff 100644 --- a/app/angular/src/client/preview/angular/helpers.ts +++ b/app/angular/src/client/preview/angular/helpers.ts @@ -5,6 +5,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './components/app.component'; import { STORY } from './app.token'; import { NgModuleMetadata, IStoryFn, NgStory } from './types'; +import { ReplaySubject } from 'rxjs'; let platform: any = null; let promises: Array>> = []; @@ -14,6 +15,8 @@ const componentClass = class DynamicComponent {}; type DynamicComponentType = typeof componentClass; +const storyData = new ReplaySubject(1); + const getModule = ( declarations: Array | any[]>, entryComponents: Array | any[]>, @@ -21,10 +24,12 @@ const getModule = ( data: NgStory, moduleMetadata: NgModuleMetadata ) => { + storyData.next(data); + const moduleMeta = { declarations: [...declarations, ...(moduleMetadata.declarations || [])], imports: [BrowserModule, FormsModule, ...(moduleMetadata.imports || [])], - providers: [{ provide: STORY, useValue: { ...data } }, ...(moduleMetadata.providers || [])], + providers: [{ provide: STORY, useValue: storyData }, ...(moduleMetadata.providers || [])], entryComponents: [...entryComponents, ...(moduleMetadata.entryComponents || [])], schemas: [...(moduleMetadata.schemas || [])], bootstrap: [...bootstrap], @@ -86,6 +91,10 @@ const draw = (newModule: DynamicComponentType): void => { } }; -export const renderNgApp = (storyFn: IStoryFn) => { - draw(initModule(storyFn)); +export const renderNgApp = (storyFn: IStoryFn, forced: boolean) => { + if (!forced) { + draw(initModule(storyFn)); + } else { + storyData.next(storyFn()); + } }; diff --git a/app/angular/src/client/preview/render.js b/app/angular/src/client/preview/render.js index c191b41d0789..c84fd11dec3c 100644 --- a/app/angular/src/client/preview/render.js +++ b/app/angular/src/client/preview/render.js @@ -1,6 +1,6 @@ import { renderNgApp } from './angular/helpers'; -export default function render({ storyFn, showMain }) { +export default function render({ storyFn, showMain, forceRender }) { showMain(); - renderNgApp(storyFn); + renderNgApp(storyFn, forceRender); } diff --git a/examples/angular-cli/src/stories/addon-knobs.stories.ts b/examples/angular-cli/src/stories/addon-knobs.stories.ts index 602feb23299a..7e75d2282365 100644 --- a/examples/angular-cli/src/stories/addon-knobs.stories.ts +++ b/examples/angular-cli/src/stories/addon-knobs.stories.ts @@ -18,7 +18,7 @@ import { SimpleKnobsComponent } from './knobs.component'; import { AllKnobsComponent } from './all-knobs.component'; storiesOf('Addon|Knobs', module) - .addDecorator(withKnobs) + .addDecorator(withKnobs({ disableDebounce: true })) .add('Simple', () => { const name = text('name', 'John Doe'); const age = number('age', 0); diff --git a/examples/angular-cli/tsconfig.json b/examples/angular-cli/tsconfig.json index d0f3d8e160b1..eefc14d2b741 100644 --- a/examples/angular-cli/tsconfig.json +++ b/examples/angular-cli/tsconfig.json @@ -10,6 +10,8 @@ "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "target": "es5", "typeRoots": ["../../node_modules/@types", "node_modules/@types"], "lib": ["es2017", "dom"] diff --git a/tslint.json b/tslint.json index 6133784fb3c8..be2adec3353f 100644 --- a/tslint.json +++ b/tslint.json @@ -18,7 +18,6 @@ "comment-format": [true, "check-space"], "curly": true, "forin": true, - "import-blacklist": [true, "rxjs"], "interface-over-type-literal": true, "label-position": true, "member-access": false,