Fix performance of Angular rerender

This commit is contained in:
Jon Rimmer 2019-03-14 18:36:19 +00:00
parent 8cfc2c2007
commit e651a3bdbc
8 changed files with 59 additions and 20 deletions

View File

@ -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) {

View File

@ -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": {

View File

@ -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<NgStory>
) {}
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;
this.subscription = this.data.subscribe(newData => {
this.setProps(instance, newData);
ref.changeDetectorRef.detectChanges();
});
});
}
ngOnDestroy(): void {
this.target.clear();
}
private putInMyHtml(): 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();
}
}
/**

View File

@ -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<Promise<NgModuleRef<any>>> = [];
@ -14,6 +15,8 @@ const componentClass = class DynamicComponent {};
type DynamicComponentType = typeof componentClass;
const storyData = new ReplaySubject(1);
const getModule = (
declarations: Array<Type<any> | any[]>,
entryComponents: Array<Type<any> | 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());
}
};

View File

@ -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);
}

View File

@ -18,6 +18,11 @@ import { SimpleKnobsComponent } from './knobs.component';
import { AllKnobsComponent } from './all-knobs.component';
storiesOf('Addon|Knobs', module)
.addParameters({
knobs: {
disableDebounce: true,
},
})
.addDecorator(withKnobs)
.add('Simple', () => {
const name = text('name', 'John Doe');

View File

@ -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"]

View File

@ -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,