Optimise core memoization of client component props to improve performance significantly

This commit is contained in:
Andrew Kingston 2021-11-16 16:29:31 +00:00
parent 6ca6eb0d4b
commit f61d89436b
1 changed files with 100 additions and 94 deletions

View File

@ -4,7 +4,7 @@
<script> <script>
import { getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import { writable, get } from "svelte/store" import { writable } from "svelte/store"
import * as AppComponents from "components/app" import * as AppComponents from "components/app"
import Router from "./Router.svelte" import Router from "./Router.svelte"
import { enrichProps, propsAreSame } from "utils/componentProps" import { enrichProps, propsAreSame } from "utils/componentProps"
@ -19,15 +19,22 @@
export let isScreen = false export let isScreen = false
export let isBlock = false export let isBlock = false
// Component settings are the un-enriched settings for this component that
// 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 nestedSettings
// The enriched component settings // The enriched component settings
let enrichedSettings let enrichedSettings
// Any prop overrides that need to be applied due to conditional UI // Any setting overrides that need to be applied due to conditional UI
let conditionalSettings let conditionalSettings
// Settings are hashed when inside the builder preview and used as a key, // Resultant cached settings which will be passed to the component instance.
// so that components fully remount whenever any settings change // These are a combination of the enriched, nested and conditional settings.
let hash = 0 let cachedSettings
// Latest timestamp that we started a props update. // Latest timestamp that we started a props update.
// Due to enrichment now being async, we need to avoid overwriting newer // Due to enrichment now being async, we need to avoid overwriting newer
@ -82,33 +89,19 @@
$: empty = interactive && !children.length && definition?.hasChildren $: empty = interactive && !children.length && definition?.hasChildren
$: emptyState = empty && definition?.showEmptyState !== false $: emptyState = empty && definition?.showEmptyState !== false
// Raw props are all props excluding internal props and children // Raw settings are all settings excluding internal props and children
$: rawSettings = getRawSettings(instance) $: rawSettings = getRawSettings(instance)
$: instanceKey = hashString(JSON.stringify(rawSettings)) $: instanceKey = hashString(JSON.stringify(rawSettings))
// Component settings are those which are intended for this component and // Update and enrich component settings
// which need to be enriched $: updateSettings(rawSettings, instanceKey, settingsDefinition, $context)
$: componentSettings = getComponentSettings(rawSettings, settingsDefinition)
$: enrichComponentSettings(rawSettings, instanceKey, $context)
// Nested settings are those which are intended for child components inside
// blocks and which should not be enriched at this level
$: nestedSettings = getNestedSettings(rawSettings, settingsDefinition)
// Evaluate conditional UI settings and store any component setting changes // Evaluate conditional UI settings and store any component setting changes
// which need to be made // which need to be made
$: evaluateConditions(enrichedSettings?._conditions) $: evaluateConditions(enrichedSettings?._conditions)
// Build up the final settings object to be passed to the component // Build up the final settings object to be passed to the component
$: settings = { $: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings)
...enrichedSettings,
...nestedSettings,
...conditionalSettings,
}
// Render key is used when in the builder preview to fully remount
// components when settings are changed
$: renderKey = `${hash}-${emptyState}`
// Update component context // Update component context
$: componentStore.set({ $: componentStore.set({
@ -128,6 +121,7 @@
editing, editing,
}) })
// Extracts all settings from the component instance
const getRawSettings = instance => { const getRawSettings = instance => {
let validSettings = {} let validSettings = {}
Object.entries(instance) Object.entries(instance)
@ -148,12 +142,14 @@
return AppComponents[name] return AppComponents[name]
} }
// Gets this component's definition from the manifest
const getComponentDefinition = component => { const getComponentDefinition = component => {
const prefix = "@budibase/standard-components/" const prefix = "@budibase/standard-components/"
const type = component?.replace(prefix, "") const type = component?.replace(prefix, "")
return type ? Manifest[type] : null return type ? Manifest[type] : null
} }
// Gets the definition of this component's settings from the manifest
const getSettingsDefinition = definition => { const getSettingsDefinition = definition => {
if (!definition) { if (!definition) {
return [] return []
@ -173,35 +169,50 @@
return settings return settings
} }
const getComponentSettings = (rawSettings, settingsDefinition) => { // Updates and enriches component settings when raw settings change
let clone = { ...rawSettings } const updateSettings = (settings, key, settingsDefinition, context) => {
settingsDefinition?.forEach(setting => { const instanceChanged = key !== lastInstanceKey
if (setting.nested) {
delete clone[setting.key] // Derive component and nested settings if the instance changed
} if (instanceChanged) {
}) splitRawSettings(settings, settingsDefinition)
return clone
} }
const getNestedSettings = (rawSettings, settingsDefinition) => { // Enrich component settings
let clone = { ...rawSettings } enrichComponentSettings(componentSettings, context, instanceChanged)
// Update instance key
if (instanceChanged) {
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 }
settingsDefinition?.forEach(setting => { settingsDefinition?.forEach(setting => {
if (!setting.nested) { if (setting.nested) {
delete clone[setting.key] delete newComponentSettings[setting.key]
} else {
delete newNestedSettings[setting.key]
} }
}) })
return clone componentSettings = newComponentSettings
nestedSettings = newNestedSettings
} }
// Enriches any string component props using handlebars // Enriches any string component props using handlebars
const enrichComponentSettings = (rawSettings, instanceKey, context) => { const enrichComponentSettings = (rawSettings, context, instanceChanged) => {
const instanceSame = instanceKey === lastInstanceKey const contextChanged = context.key !== lastContextKey
const contextSame = context.key === lastContextKey
if (instanceSame && contextSame) { // Skip enrichment if the context and instance are unchanged
if (!contextChanged) {
if (!instanceChanged) {
return return
}
} else { } else {
lastInstanceKey = instanceKey
lastContextKey = context.key lastContextKey = context.key
} }
@ -217,31 +228,11 @@
return return
} }
// Update the component props. enrichedSettings = newEnrichedSettings
// Most props are deeply compared so that svelte will only trigger reactive
// statements on props that have actually changed.
if (!newEnrichedSettings) {
return
}
let propsChanged = false
if (!enrichedSettings) {
enrichedSettings = {}
propsChanged = true
}
Object.keys(newEnrichedSettings).forEach(key => {
if (!propsAreSame(newEnrichedSettings[key], enrichedSettings[key])) {
propsChanged = true
enrichedSettings[key] = newEnrichedSettings[key]
}
})
// Update the hash if we're in the builder so we can fully remount this
// component
if (get(builderStore).inBuilder && propsChanged) {
hash = hashString(JSON.stringify(enrichedSettings))
}
} }
// Evaluates the list of conditional UI conditions and determines any setting
// or visibility changes required
const evaluateConditions = conditions => { const evaluateConditions = conditions => {
if (!conditions?.length) { if (!conditions?.length) {
return return
@ -261,10 +252,26 @@
conditionalSettings = result.settingUpdates conditionalSettings = result.settingUpdates
visible = nextVisible visible = nextVisible
} }
// 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 }
if (!cachedSettings) {
cachedSettings = allSettings
} else {
Object.keys(allSettings).forEach(key => {
if (!propsAreSame(allSettings[key], cachedSettings[key])) {
console.log("new '" + key + "' setting for '" + id + "'")
cachedSettings[key] = allSettings[key]
}
})
}
}
</script> </script>
{#key renderKey} {#if constructor && cachedSettings && (visible || inSelectedPath)}
{#if constructor && settings && (visible || inSelectedPath)}
<!-- The ID is used as a class because getElementsByClassName is O(1) --> <!-- The ID is used as a class because getElementsByClassName is O(1) -->
<!-- and the performance matters for the selection indicators --> <!-- and the performance matters for the selection indicators -->
<div <div
@ -278,7 +285,7 @@
data-id={id} data-id={id}
data-name={name} data-name={name}
> >
<svelte:component this={constructor} {...settings}> <svelte:component this={constructor} {...cachedSettings}>
{#if children.length} {#if children.length}
{#each children as child (child._id)} {#each children as child (child._id)}
<svelte:self instance={child} /> <svelte:self instance={child} />
@ -290,8 +297,7 @@
{/if} {/if}
</svelte:component> </svelte:component>
</div> </div>
{/if} {/if}
{/key}
<style> <style>
.component { .component {