Optimise core memoization of client component props to improve performance significantly
This commit is contained in:
parent
6ca6eb0d4b
commit
f61d89436b
|
@ -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
|
}
|
||||||
|
|
||||||
|
// Enrich component settings
|
||||||
|
enrichComponentSettings(componentSettings, context, instanceChanged)
|
||||||
|
|
||||||
|
// Update instance key
|
||||||
|
if (instanceChanged) {
|
||||||
|
lastInstanceKey = key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNestedSettings = (rawSettings, settingsDefinition) => {
|
// Splits the raw settings into those destined for the component itself
|
||||||
let clone = { ...rawSettings }
|
// 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
|
||||||
return
|
if (!contextChanged) {
|
||||||
|
if (!instanceChanged) {
|
||||||
|
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,37 +252,52 @@
|
||||||
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
|
class={`component ${id}`}
|
||||||
class={`component ${id}`}
|
class:draggable
|
||||||
class:draggable
|
class:droppable
|
||||||
class:droppable
|
class:empty
|
||||||
class:empty
|
class:interactive
|
||||||
class:interactive
|
class:editing
|
||||||
class:editing
|
class:block={isBlock}
|
||||||
class:block={isBlock}
|
data-id={id}
|
||||||
data-id={id}
|
data-name={name}
|
||||||
data-name={name}
|
>
|
||||||
>
|
<svelte:component this={constructor} {...cachedSettings}>
|
||||||
<svelte:component this={constructor} {...settings}>
|
{#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} />
|
{/each}
|
||||||
{/each}
|
{:else if emptyState}
|
||||||
{:else if emptyState}
|
<Placeholder />
|
||||||
<Placeholder />
|
{:else if isBlock}
|
||||||
{:else if isBlock}
|
<slot />
|
||||||
<slot />
|
{/if}
|
||||||
{/if}
|
</svelte:component>
|
||||||
</svelte:component>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.component {
|
.component {
|
||||||
|
|
Loading…
Reference in New Issue