From e7b02aec0409e6d00f461b13123ee276ac32ba00 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 29 Jan 2022 18:53:21 +0000 Subject: [PATCH] Add experimental support for caching the creation of HBS template functions --- .../client/src/components/Component.svelte | 89 ++++++++++++------- .../client/src/utils/enrichDataBinding.js | 2 +- packages/string-templates/src/index.js | 43 ++++++--- 3 files changed, 88 insertions(+), 46 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index f43c2b30ec..71e08f1215 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -33,9 +33,12 @@ // need to be enriched at this level. // Nested settings are the un-enriched block settings that are to be passed on // and enriched at a deeper level. - let componentSettings + let dynamicSettings + let staticSettings let nestedSettings + // The context keys that + // The enriched component settings let enrichedSettings @@ -108,15 +111,23 @@ $: rawSettings = getRawSettings(instance) $: instanceKey = Helpers.hashString(JSON.stringify(rawSettings)) - // Update and enrich component settings - $: updateSettings(rawSettings, instanceKey, settingsDefinition, $context) + // Parse and split component settings into categories + $: updateSettings(rawSettings, instanceKey, settingsDefinition) + + // Enrich component settings + $: enrichComponentSettings(dynamicSettings, $context) // Evaluate conditional UI settings and store any component setting changes // which need to be made $: evaluateConditions(enrichedSettings?._conditions) // Build up the final settings object to be passed to the component - $: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings) + $: cacheSettings( + staticSettings, + enrichedSettings, + nestedSettings, + conditionalSettings + ) // Update component context $: componentStore.set({ @@ -185,48 +196,55 @@ } // Updates and enriches component settings when raw settings change - const updateSettings = (settings, key, settingsDefinition, context) => { + const updateSettings = (settings, key, settingsDefinition) => { const instanceChanged = key !== lastInstanceKey - - // Derive component and nested settings if the instance changed - if (instanceChanged) { - splitRawSettings(settings, settingsDefinition) - } - - // Enrich component settings - enrichComponentSettings(componentSettings, context, instanceChanged) - - // Update instance key - if (instanceChanged) { + if (!instanceChanged) { + return + } else { lastInstanceKey = key } - } - // Splits the raw settings into those destined for the component itself - // and nexted settings for child components inside blocks - const splitRawSettings = (rawSettings, settingsDefinition) => { - let newComponentSettings = { ...rawSettings } - let newNestedSettings = { ...rawSettings } + // Derive static, dynamic and nested settings if the instance changed + let newStaticSettings = { ...settings } + let newDynamicSettings = { ...settings } + let newNestedSettings = { ...settings } settingsDefinition?.forEach(setting => { if (setting.nested) { - delete newComponentSettings[setting.key] + delete newStaticSettings[setting.key] + delete newDynamicSettings[setting.key] } else { delete newNestedSettings[setting.key] + + // This is a non-nested setting, but we need to find out if it is + // static or dynamic + const value = rawSettings[setting.key] + if (value == null) { + delete newDynamicSettings[setting.key] + } else if (typeof value === "string" && value.includes("{{")) { + delete newStaticSettings[setting.key] + } else if (typeof value === "object") { + const stringified = JSON.stringify(value) + if (stringified.includes("{{")) { + delete newStaticSettings[setting.key] + } else { + delete newDynamicSettings[setting.key] + } + } else { + delete newDynamicSettings[setting.key] + } } }) - componentSettings = newComponentSettings + + staticSettings = newStaticSettings + dynamicSettings = newDynamicSettings nestedSettings = newNestedSettings } // Enriches any string component props using handlebars - const enrichComponentSettings = (rawSettings, context, instanceChanged) => { + const enrichComponentSettings = (settings, context) => { const contextChanged = context.key !== lastContextKey - - // Skip enrichment if the context and instance are unchanged if (!contextChanged) { - if (!instanceChanged) { - return - } + return } else { lastContextKey = context.key } @@ -236,7 +254,7 @@ const enrichmentTime = latestUpdateTime // Enrich settings with context - const newEnrichedSettings = enrichProps(rawSettings, context) + const newEnrichedSettings = enrichProps(settings, context) // Abandon this update if a newer update has started if (enrichmentTime !== latestUpdateTime) { @@ -271,8 +289,13 @@ // Combines and caches all settings which will be passed to the component // instance. Settings are aggressively memoized to avoid triggering svelte // reactive statements as much as possible. - const cacheSettings = (enriched, nested, conditional) => { - const allSettings = { ...enriched, ...nested, ...conditional } + const cacheSettings = (staticSettings, enriched, nested, conditional) => { + const allSettings = { + ...staticSettings, + ...enriched, + ...nested, + ...conditional, + } if (!cachedSettings) { cachedSettings = { ...allSettings } initialSettings = cachedSettings diff --git a/packages/client/src/utils/enrichDataBinding.js b/packages/client/src/utils/enrichDataBinding.js index 34bfb78539..fd7f9bc576 100644 --- a/packages/client/src/utils/enrichDataBinding.js +++ b/packages/client/src/utils/enrichDataBinding.js @@ -24,5 +24,5 @@ export const enrichDataBinding = async (input, context) => { * Props are deeply cloned so that no mutation is done to the source object. */ export const enrichDataBindings = (props, context) => { - return processObjectSync(cloneDeep(props), context) + return processObjectSync(cloneDeep(props), context, { cache: true }) } diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js index d824d5f1db..000965295a 100644 --- a/packages/string-templates/src/index.js +++ b/packages/string-templates/src/index.js @@ -7,10 +7,10 @@ const manifest = require("../manifest.json") const hbsInstance = handlebars.create() registerAll(hbsInstance) const hbsInstanceNoHelpers = handlebars.create() -const defaultOpts = { noHelpers: false } +const defaultOpts = { noHelpers: false, cacheTemplates: false } /** - * utility function to check if the object is valid + * Utility function to check if the object is valid. */ function testObject(object) { // JSON stringify will fail if there are any cycles, stops infinite recursion @@ -21,6 +21,32 @@ function testObject(object) { } } +/** + * Creates a HBS template function for a given string, and optionally caches it. + */ +let templateCache = {} +function createTemplate(string, noHelpers, cache) { + // Finalising adds a helper, can't do this with no helpers + const shouldFinalise = !noHelpers + const key = `${string}${shouldFinalise}` + + // Reuse the cached template is possible + if (cache && templateCache[key]) { + return templateCache[key] + } + + string = processors.preprocess(string, shouldFinalise) + + // This does not throw an error when template can't be fulfilled, + // have to try correct beforehand + const instance = noHelpers ? hbsInstanceNoHelpers : hbsInstance + const template = instance.compile(string, { + strict: false, + }) + templateCache[key] = template + return template +} + /** * Given an input object this will recurse through all props to try and update any handlebars statements within. * @param {object|array} object The input structure which is to be recursed, it is important to note that @@ -104,14 +130,7 @@ module.exports.processStringSync = (string, context, opts) => { throw "Cannot process non-string types." } try { - // finalising adds a helper, can't do this with no helpers - const shouldFinalise = !opts.noHelpers - string = processors.preprocess(string, shouldFinalise) - // this does not throw an error when template can't be fulfilled, have to try correct beforehand - const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance - const template = instance.compile(string, { - strict: false, - }) + const template = createTemplate(string, opts.noHelpers, opts.cacheTemplates) const now = Math.floor(Date.now() / 1000) * 1000 return processors.postprocess( template({ @@ -154,8 +173,8 @@ module.exports.isValid = (string, opts) => { // don't really need a real context to check if its valid const context = {} try { - const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance - instance.compile(processors.preprocess(string, false))(context) + const template = createTemplate(string, opts.noHelpers, opts.cache) + template(context) return true } catch (err) { const msg = err && err.message ? err.message : err