Skip to content

Commit

Permalink
perf(ivy): apply [class] bindings directly to className
Browse files Browse the repository at this point in the history
This patch ensures that `[class]` based bindings are directly
applied to an element's className property.

This patch optimizes the algorithm so that it...
- Doesn't construct an update an instance of `StylingMapArray` for
  `[style]` and `[class]` bindings
- Doesn't appled `[class]` based entries using `classList`
- Doesn't split or iterate over all string-based tokens in a
  string value obtained from a `[class]` binding.

This patch speeds up the `<div [class]>` and `<div class="" [class]>`
case by over 5000x.
  • Loading branch information
matsko committed Oct 22, 2019
1 parent 1b8b04c commit 5cc4e12
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 20 deletions.
11 changes: 5 additions & 6 deletions packages/core/src/render3/instructions/styling.ts
Expand Up @@ -334,7 +334,6 @@ function stylingMap(
elementIndex: number, context: TStylingContext, bindingIndex: number,
value: {[key: string]: any} | string | null, isClassBased: boolean): boolean {
let updated = false;

const lView = getLView();
const directiveIndex = getActiveDirectiveId();
const tNode = getTNode(elementIndex, lView);
Expand All @@ -359,24 +358,24 @@ function stylingMap(
patchConfig(context, TStylingConfig.HasMapBindings);
}

const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);

// Direct Apply Case: bypass context resolution and apply the
// style/class map values directly to the element
if (allowDirectStyling(context, hostBindingsMode)) {
const sanitizerToUse = isClassBased ? null : sanitizer;
const renderer = getRenderer(tNode, lView);
updated = applyStylingMapDirectly(
renderer, context, native, lView, bindingIndex, stylingMapArr as StylingMapArray,
isClassBased, isClassBased ? setClass : setStyle, sanitizerToUse, valueHasChanged);
renderer, context, native, lView, bindingIndex, value, isClassBased,
isClassBased ? setClass : setStyle, sanitizerToUse, valueHasChanged);
if (sanitizerToUse) {
// it's important we remove the current style sanitizer once the
// element exits, otherwise it will be used by the next styling
// instructions for the next element.
setElementExitFn(stylingApply);
}
} else {
const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);

updated = valueHasChanged;
activateStylingMapFeature();

Expand Down
54 changes: 47 additions & 7 deletions packages/core/src/render3/styling/bindings.ts
Expand Up @@ -10,7 +10,7 @@ import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanit
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, normalizeIntoStylingMap, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';

import {getStylingState, resetStylingState} from './state';

Expand Down Expand Up @@ -655,12 +655,28 @@ export function applyStylingViaContext(
*/
export function applyStylingMapDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, map: StylingMapArray, isClassBased: boolean, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
if (forceUpdate || hasValueChanged(data[bindingIndex], map)) {
setValue(data, bindingIndex, map);
const initialStyles =
hasConfig(context, TStylingConfig.HasInitialStyling) ? getStylingMapArray(context) : null;
bindingIndex: number, value: {[key: string]: any} | string | null, isClassBased: boolean,
applyFn: ApplyStylingFn, sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
const config = getConfig(context);
const writeToClassNameDirectly = isClassBased && !(config & TStylingConfig.HasPropBindings);
const oldValue = getValue(data, bindingIndex);
if (!writeToClassNameDirectly && value !== NO_CHANGE) {
value = normalizeIntoStylingMap(oldValue, value, !isClassBased);
}

if (forceUpdate || hasValueChanged(oldValue, value)) {
const hasInitial = config & TStylingConfig.HasInitialStyling;
setValue(data, bindingIndex, value);

if (writeToClassNameDirectly) {
let classNameValue = hasInitial ? getInitialStylingValue(context) + ' ' : '';
classNameValue += typeof value === 'string' ? value : objectToClassName(value);
setClassName(renderer, element, classNameValue, null, bindingIndex);
return true;
}

const map = value as StylingMapArray;
const initialStyles = hasInitial ? getStylingMapArray(context) : null;

for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
Expand Down Expand Up @@ -888,6 +904,17 @@ export const setClass: ApplyStylingFn =
}
};

export const setClassName: ApplyStylingFn =
(renderer: Renderer3 | null, native: RElement, className: string, value: any) => {
if (renderer !== null) {
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(native, 'class', className);
} else {
native.className = className;
}
}
};

/**
* Iterates over all provided styling entries and renders them on the element.
*
Expand All @@ -914,3 +941,16 @@ export function renderStylingMap(
}
}
}

function objectToClassName(obj: {[key: string]: any} | null): string {
let str = '';
if (obj) {
for (let key in obj) {
const value = obj[key];
if (value) {
str += (str.length ? ' ' : '') + key;
}
}
}
return str;
}
74 changes: 67 additions & 7 deletions packages/core/src/render3/styling/styling_debug.ts
Expand Up @@ -5,12 +5,13 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {createProxy} from '../../debug/proxy';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling';
import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils';
import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext} from '../util/styling_utils';
import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValue, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext, normalizeIntoStylingMap, setValue} from '../util/styling_utils';

import {applyStylingViaContext} from './bindings';
import {activateStylingMapFeature} from './map_based_bindings';
Expand Down Expand Up @@ -374,10 +375,52 @@ export class NodeStylingDebug implements DebugNodeStyling {
*/
get summary(): {[key: string]: DebugNodeStylingEntry} {
const entries: {[key: string]: DebugNodeStylingEntry} = {};
this._mapValues((prop: string, value: any, bindingIndex: number | null) => {
const config = this.config;
const isClassBased = this._isClassBased;

let data = this._data;

// the direct pass code doesn't convert [style] or [class] values
// into StylingMapArray instances. For this reason, the values
// need to be converted ahead of time since the styling debug
// relies on context resolution to figure out what styling
// values have been added/removed on the element.
if (config.allowDirectStyling && config.hasMapBindings) {
data = data.concat([]); // make a copy
this._convertMapBindingsToStylingMapArrays(data);
}

this._mapValues(data, (prop: string, value: any, bindingIndex: number | null) => {
entries[prop] = {prop, value, bindingIndex};
});
return entries;

// because the styling algorithm runs into two different
// modes: direct and context-resolution, the output of the entries
// object is different because the removed values are not
// saved between updates. For this reason a proxy is created
// so that the behavior is the same when examining values
// that are no longer active on the element.
return createProxy({
get(target: {}, prop: string): DebugNodeStylingEntry{
let value: DebugNodeStylingEntry = entries[prop]; if (!value) {
value = {
prop,
value: isClassBased ? false : null,
bindingIndex: null,
};
} return value;
},
set(target: {}, prop: string, value: any) { return false; },
ownKeys() { return Object.keys(entries); },
getOwnPropertyDescriptor(k: any) {
// we use a special property descriptor here so that enumeration operations
// such as `Object.keys` will work on this proxy.
return {
enumerable: true,
configurable: true,
};
},
});
}

get config() { return buildConfig(this.context.context); }
Expand All @@ -387,11 +430,28 @@ export class NodeStylingDebug implements DebugNodeStyling {
*/
get values(): {[key: string]: any} {
const entries: {[key: string]: any} = {};
this._mapValues((prop: string, value: any) => { entries[prop] = value; });
this._mapValues(this._data, (prop: string, value: any) => { entries[prop] = value; });
return entries;
}

private _mapValues(fn: (prop: string, value: string|null, bindingIndex: number|null) => any) {
private _convertMapBindingsToStylingMapArrays(data: LStylingData) {
const context = this.context.context;
const limit = getPropValuesStartPosition(context);
for (let i =
TStylingContextIndex.ValuesStartPosition + TStylingContextIndex.BindingsStartOffset;
i < limit; i++) {
const bindingIndex = context[i] as number;
const bindingValue = bindingIndex !== 0 ? getValue(data, bindingIndex) : null;
if (bindingValue && !Array.isArray(bindingValue)) {
const stylingMapArray = normalizeIntoStylingMap(null, bindingValue, !this._isClassBased);
setValue(data, bindingIndex, stylingMapArray);
}
}
}

private _mapValues(
data: LStylingData,
fn: (prop: string, value: string|null, bindingIndex: number|null) => any) {
// there is no need to store/track an element instance. The
// element is only used when the styling algorithm attempts to
// style the value (and we mock out the stylingApplyFn anyway).
Expand All @@ -409,11 +469,11 @@ export class NodeStylingDebug implements DebugNodeStyling {

// run the template bindings
applyStylingViaContext(
this.context.context, null, mockElement, this._data, true, mapFn, sanitizer, false);
this.context.context, null, mockElement, data, true, mapFn, sanitizer, false);

// and also the host bindings
applyStylingViaContext(
this.context.context, null, mockElement, this._data, true, mapFn, sanitizer, true);
this.context.context, null, mockElement, data, true, mapFn, sanitizer, true);
}
}

Expand Down

0 comments on commit 5cc4e12

Please sign in to comment.