aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2023-09-09 18:43:18 +0100
committerHayleigh Thompson <me@hayleigh.dev>2023-09-09 18:43:18 +0100
commit36b4cbf432bdccbc0d3c16692d913e4d1e263dfa (patch)
tree8a81901edbd1d413832cfc4699490a8ff7518584
parent6d295d0bcf23bb4410d6d93435ce53f9670a1d1c (diff)
downloadlustre-36b4cbf432bdccbc0d3c16692d913e4d1e263dfa.tar.gz
lustre-36b4cbf432bdccbc0d3c16692d913e4d1e263dfa.zip
:zap: Improve render performance.
-rw-r--r--lib/src/lustre.ffi.mjs30
-rw-r--r--lib/src/runtime.ffi.mjs88
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 ------------------------------------------------------------------------