diff --git a/src/core/directives/text.ts b/src/core/directives/text.ts index 247f9f9..713e6b7 100644 --- a/src/core/directives/text.ts +++ b/src/core/directives/text.ts @@ -1,8 +1,9 @@ import { DirectiveProps } from '../../models/structs'; -export const textDirective = ({ el, data, state }: DirectiveProps): void => { +export const textDirective = ({ el, parts, data, state }: DirectiveProps): void => { const ret = data.compute(state) ?? data.value; - if (ret !== el.innerText) { - el.innerText = ret; + const prop = parts[1] === 'perf' ? 'textContent' : 'innerText'; + if (ret !== el[prop]) { + el[prop] = ret; } }; diff --git a/src/core/render.ts b/src/core/render.ts index 6d13ad3..cfd202f 100644 --- a/src/core/render.ts +++ b/src/core/render.ts @@ -1,6 +1,7 @@ import { DIRECTIVE_PREFIX, UnknownKV } from '../models/generics'; import { ASTNode, ASTNodeType, Directives } from '../models/structs'; import { renderDirective } from './directive'; +import lazy from './utils/lazy'; import { rawDirectiveSplitRE } from './utils/patterns'; const render = ( @@ -10,54 +11,59 @@ const render = ( changedProps: string[] = [] ): void => { const legalDirectiveNames = Object.keys(directives); + const LAZY_MODE_TIMEOUT = 25; - for (const node of ast) { - if (node.type === ASTNodeType.NULL) continue; - const isStatic = node.type === ASTNodeType.STATIC; - if (isStatic) node.type = ASTNodeType.NULL; - - const nodeHasDep = changedProps.some((prop) => node.deps.includes(prop)); - - if (!nodeHasDep && !isStatic) continue; - - for (const [directiveName, directiveData] of Object.entries(node.directives)) { - const rawDirectiveName = directiveName.split(rawDirectiveSplitRE())[0]; - // Validate if it is a legal directive - if (!legalDirectiveNames.includes(rawDirectiveName.toUpperCase())) continue; - // Iterate through affected and check if directive value has prop - const directiveHasDep = changedProps.some((prop) => directiveData.deps.includes(prop)); - - const isMaskDirective = directiveName === `${DIRECTIVE_PREFIX}mask`; - const isStaticDirective = Object.keys(directiveData.deps).length === 0; - - // If affected, then push to render queue - if (directiveHasDep || isStatic || isStaticDirective) { - const directiveProps = { - el: node.el, - parts: directiveName.split(rawDirectiveSplitRE()), - data: directiveData, - node, - state, - }; - - renderDirective(directiveProps, directives); - - if (isStaticDirective || isMaskDirective) { - delete node.directives[directiveName]; - if (isMaskDirective) { - /* istanbul ignore next */ - node.el.removeAttribute(`${DIRECTIVE_PREFIX}mask`); + lazy(LAZY_MODE_TIMEOUT, function* () { + for (const node of ast) { + if (node.type === ASTNodeType.NULL) continue; + const isStatic = node.type === ASTNodeType.STATIC; + if (isStatic) node.type = ASTNodeType.NULL; + yield; + + const nodeHasDep = changedProps.some((prop) => node.deps.includes(prop)); + + if (!nodeHasDep && !isStatic) continue; + + for (const [directiveName, directiveData] of Object.entries(node.directives)) { + const rawDirectiveName = directiveName.split(rawDirectiveSplitRE())[0]; + // Validate if it is a legal directive + if (!legalDirectiveNames.includes(rawDirectiveName.toUpperCase())) continue; + yield; + // Iterate through affected and check if directive value has prop + const directiveHasDep = changedProps.some((prop) => directiveData.deps.includes(prop)); + + const isMaskDirective = directiveName === `${DIRECTIVE_PREFIX}mask`; + const isStaticDirective = Object.keys(directiveData.deps).length === 0; + + // If affected, then push to render queue + if (directiveHasDep || isStatic || isStaticDirective) { + const directiveProps = { + el: node.el, + parts: directiveName.split(rawDirectiveSplitRE()), + data: directiveData, + node, + state, + }; + + renderDirective(directiveProps, directives); + + if (isStaticDirective || isMaskDirective) { + delete node.directives[directiveName]; + if (isMaskDirective) { + /* istanbul ignore next */ + node.el.removeAttribute(`${DIRECTIVE_PREFIX}mask`); + } } } } - } - // Effect is like a watcher but detects changes to an el - if (node.directives['on:effect']) { - const effectEvent = new CustomEvent('effect'); - node.el.dispatchEvent(effectEvent); + // Effect is like a watcher but detects changes to an el + if (node.directives['on:effect']) { + const effectEvent = new CustomEvent('effect'); + node.el.dispatchEvent(effectEvent); + } } - } + })(); }; export default render; diff --git a/src/core/utils/lazy.ts b/src/core/utils/lazy.ts new file mode 100644 index 0000000..bd302c1 --- /dev/null +++ b/src/core/utils/lazy.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ + +// Lazy allows us to delay render calls if the main thread is blocked +// This is kind of like time slicing in React but less advanced + +export const lazy = ( + threshold: number, + generatorFunction: () => Generator + // eslint-disable-next-line @typescript-eslint/ban-types +): Function => { + const generator = generatorFunction(); + return function next() { + const start = performance.now(); + let task = null; + do { + task = generator.next(); + } while (performance.now() - start < threshold && !task.done); + + if (task.done) return; + /* istanbul ignore next */ + setTimeout(next); + }; +}; + +export default lazy;