diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2023-09-09 18:43:18 +0100 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2023-09-09 18:43:18 +0100 |
commit | 36b4cbf432bdccbc0d3c16692d913e4d1e263dfa (patch) | |
tree | 8a81901edbd1d413832cfc4699490a8ff7518584 | |
parent | 6d295d0bcf23bb4410d6d93435ce53f9670a1d1c (diff) | |
download | lustre-36b4cbf432bdccbc0d3c16692d913e4d1e263dfa.tar.gz lustre-36b4cbf432bdccbc0d3c16692d913e4d1e263dfa.zip |
:zap: Improve render performance.
-rw-r--r-- | lib/src/lustre.ffi.mjs | 30 | ||||
-rw-r--r-- | lib/src/runtime.ffi.mjs | 88 |
2 files changed, 55 insertions, 63 deletions
diff --git a/lib/src/lustre.ffi.mjs b/lib/src/lustre.ffi.mjs index 29a60a9..f29e7ea 100644 --- a/lib/src/lustre.ffi.mjs +++ b/lib/src/lustre.ffi.mjs @@ -1,12 +1,12 @@ import { AppAlreadyStarted, AppNotYetStarted, + BadComponentName, ComponentAlreadyRegistered, ElementNotFound, NotABrowser, } from "./lustre.mjs"; import { from } from "./lustre/effect.mjs"; -import { map } from "./lustre/element.mjs"; import { morph } from "./runtime.ffi.mjs"; import { Ok, Error, isEqual } from "./gleam.mjs"; @@ -81,32 +81,32 @@ export class App { this.#view = () => {}; } - #render() { - const node = this.#view(this.#state); - const vdom = map(node, (msg) => this.dispatch(msg)); - - this.#root = morph(this.#root, vdom); - } - #tick() { this.#flush(); - this.#didUpdate && this.#render(); - this.#didUpdate = false; + + if (this.#didUpdate) { + const vdom = this.#view(this.#state); + + this.#root = morph(this.#root, vdom, (msg) => this.dispatch(msg)); + this.#didUpdate = false; + } } #flush(times = 0) { + if (!this.#root) return; if (this.#queue.length) { while (this.#queue.length) { const [next, effects] = this.#update(this.#state, this.#queue.shift()); - + // If the user returned their model unchanged and not reconstructed then + // we don't need to trigger a re-render. + this.#didUpdate ||= this.#state !== next; this.#state = next; this.#effects = this.#effects.concat(effects[0].toArray()); } - this.#didUpdate = true; } // Each update can produce effects which must now be executed. - while (this.#effects[0]) + while (this.#effects.length) this.#effects.shift()( (msg) => this.dispatch(msg), (name, data) => this.emit(name, data) @@ -146,6 +146,7 @@ export const setup_component = ( render, on_attribute_change ) => { + if (!name.includes("-")) return new Error(new BadComponentName()); if (!is_browser()) return new Error(new NotABrowser()); if (customElements.get(name)) { return new Error(new ComponentAlreadyRegistered()); @@ -206,7 +207,7 @@ export const setup_component = ( } connectedCallback() { - this.appendChild(this.#container.firstElementChild); + this.appendChild(this.#container.firstChild); } attributeChangedCallback(name, prev, next) { @@ -220,6 +221,7 @@ export const setup_component = ( } } ); + return new Ok(null); }; diff --git a/lib/src/runtime.ffi.mjs b/lib/src/runtime.ffi.mjs index 59c5883..8986d7f 100644 --- a/lib/src/runtime.ffi.mjs +++ b/lib/src/runtime.ffi.mjs @@ -1,21 +1,22 @@ -import { List, Empty } from "./gleam.mjs"; -import { Some, None } from "../gleam_stdlib/gleam/option.mjs"; +import { Empty } from "./gleam.mjs"; +import { map as result_map } from "../gleam_stdlib/gleam/result.mjs"; -export function morph(prev, curr, parent) { - if (curr[3]) +export function morph(prev, curr, dispatch, parent) { + if (curr[3]) { return prev?.nodeType === 1 && prev.nodeName === curr[0].toUpperCase() && prev.namespaceURI === curr[3] - ? morphElement(prev, curr, curr[3], parent) - : createElement(prev, curr, curr[3], parent); + ? morphElement(prev, curr, curr[3], dispatch, parent) + : createElement(prev, curr, curr[3], dispatch, parent); + } if (curr[2]) { return prev?.nodeType === 1 && prev.nodeName === curr[0].toUpperCase() - ? morphElement(prev, curr, null, parent) - : createElement(prev, curr, null, parent); + ? morphElement(prev, curr, null, dispatch, parent) + : createElement(prev, curr, null, dispatch, parent); } - if (curr[0]) { + if (curr[0] && typeof curr[0] === "string") { return prev?.nodeType === 3 ? morphText(prev, curr) : createText(prev, curr); @@ -33,11 +34,13 @@ export function morph(prev, curr, parent) { // ELEMENTS -------------------------------------------------------------------- -function createElement(prev, curr, ns, parent = null) { +function createElement(prev, curr, ns, dispatch, parent = null) { const el = ns ? document.createElementNS(ns, curr[0]) : document.createElement(curr[0]); + el.$lustre = {}; + let attr = curr[1]; while (attr.head) { morphAttr( @@ -45,7 +48,8 @@ function createElement(prev, curr, ns, parent = null) { attr.head[0], attr.head[0] === "class" && el.className ? `${el.className} ${attr.head[1]}` - : attr.head[1] + : attr.head[1], + dispatch ); attr = attr.tail; @@ -67,13 +71,13 @@ function createElement(prev, curr, ns, parent = null) { } while (child.head) { - el.appendChild(morph(null, child.head, el)); + el.appendChild(morph(null, child.head, dispatch, el)); child = child.tail; } } else { let child = curr[2]; while (child.head) { - el.appendChild(morph(null, child.head, el)); + el.appendChild(morph(null, child.head, dispatch, el)); child = child.tail; } @@ -83,7 +87,7 @@ function createElement(prev, curr, ns, parent = null) { return el; } -function morphElement(prev, curr, ns, parent) { +function morphElement(prev, curr, ns, dispatch, parent) { const prevAttrs = prev.attributes; const currAttrs = new Map(); @@ -106,14 +110,14 @@ function morphElement(prev, curr, ns, parent) { const value = currAttrs.get(name); if (value !== prevValue) { - morphAttr(prev, name, value); + morphAttr(prev, name, value, dispatch); currAttrs.delete(name); } } } for (const [name, value] of currAttrs) { - morphAttr(prev, name, value); + morphAttr(prev, name, value, dispatch); } if (customElements.get(curr[0])) { @@ -134,7 +138,7 @@ function morphElement(prev, curr, ns, parent) { while (prevChild) { if (currChild.head) { - morph(prevChild, currChild.head, prev); + morph(prevChild, currChild.head, dispatch, prev); currChild = currChild.tail; } @@ -142,7 +146,7 @@ function morphElement(prev, curr, ns, parent) { } while (currChild.head) { - prev.appendChild(morph(null, currChild.head, prev)); + prev.appendChild(morph(null, currChild.head, dispatch, prev)); currChild = currChild.tail; } } else { @@ -152,7 +156,7 @@ function morphElement(prev, curr, ns, parent) { while (prevChild) { if (currChild.head) { const next = prevChild.nextSibling; - morph(prevChild, currChild.head, prev); + morph(prevChild, currChild.head, dispatch, prev); currChild = currChild.tail; prevChild = next; } else { @@ -163,7 +167,7 @@ function morphElement(prev, curr, ns, parent) { } while (currChild.head) { - prev.appendChild(morph(null, currChild.head, prev)); + prev.appendChild(morph(null, currChild.head, dispatch, prev)); currChild = currChild.tail; } } @@ -173,51 +177,37 @@ function morphElement(prev, curr, ns, parent) { // ATTRIBUTES ------------------------------------------------------------------ -function morphAttr(el, name, value) { +function morphAttr(el, name, value, dispatch) { switch (typeof value) { case "string": if (el.getAttribute(name) !== value) el.setAttribute(name, value); break; - // Boolean attributes work a bit differently in HTML. Their presence always - // implies true: to set an attribute to false you need to remove it entirely. - case "boolean": - value ? el.setAttribute(name, name) : el.removeAttribute(name); - break; - // Event listeners need to be handled slightly differently because we need // to be able to support custom events. We case name.startsWith("on") && "function": { + if (el.$lustre[name] === value) break; + const event = name.slice(2).toLowerCase(); + const handler = (e) => result_map(value(e), dispatch); - if (el[`_${name}`] === value) break; + console.log(el.dataset); - el.removeEventListener(event, el[`_${name}`]); - el.addEventListener(event, value); - el[`_${name}`] = value; - break; - } + if (el.$lustre[`${name}Handler`]) { + el.removeEventListener(event, el.$lustre[`${name}Handler`]); + } - default: { - el[name] = toJsValue(value); - } - } -} + el.addEventListener(event, handler); -function toJsValue(value) { - if (value instanceof List) { - return value.toArray().map(toJsValue); - } + el.$lustre[name] = value; + el.$lustre[`${name}Handler`] = handler; - if (value instanceof Some) { - return toJsValue(value[0]); - } + break; + } - if (value instanceof None) { - return null; + default: + el[name] = value; } - - return value; } // TEXT ------------------------------------------------------------------------ |