import { T, t } from "./utils/index.js"; export const info= { title: t`Performance Optimization`, fullTitle: t`Performance Optimization with dd<el>`, description: t`Techniques for optimizing your dd<el> applications, focusing on memoization and efficient rendering.`, }; import { el } from "deka-dom-el"; import { simplePage } from "./layout/simplePage.html.js"; import { example } from "./components/example.html.js"; import { h3 } from "./components/pageUtils.html.js"; import { code } from "./components/code.html.js"; import { mnemonic } from "./components/mnemonic/optimization-init.js"; /** @param {string} url */ const fileURL= url=> new URL(url, import.meta.url); const references= { /** memo documentation */ memo_docs: { title: t`dd<el> memo API documentation`, href: "https://github.com/jaandrle/deka-dom-el#memo-api", }, /** AbortController */ mdn_abort: { title: t`MDN documentation for AbortController`, href: "https://developer.mozilla.org/en-US/docs/Web/API/AbortController", }, /** Performance API */ mdn_perf: { title: t`MDN documentation for Web Performance API`, href: "https://developer.mozilla.org/en-US/docs/Web/API/Performance_API", }, /** Virtual DOM */ virtual_dom: { title: t`Virtual DOM concept explanation`, href: "https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom", }, /** DocumentFragment */ mdn_fragment: { title: t`MDN documentation for DocumentFragment`, href: "https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment", } }; /** @param {import("./types.d.ts").PageAttrs} attrs */ export function page({ pkg, info }){ return el(simplePage, { info, pkg }).append( el("p").append(T` As your applications grow, performance becomes increasingly important. dd<el> provides several techniques to optimize rendering performance, especially when dealing with large lists or frequently updating components. This guide focuses on memoization and other optimization strategies. `), el("div", { className: "callout" }).append( el("h4", t`dd<el> Performance Optimization: Key Benefits`), el("ul").append( el("li", t`Efficient memoization system for component reuse`), el("li", t`Targeted re-rendering without virtual DOM overhead`), el("li", t`Memory management through AbortSignal integration`), el("li", t`Optimized signal updates for reactive UI patterns`), el("li", t`Simple debugging for performance bottlenecks`) ) ), el(code, { src: fileURL("./components/examples/optimization/intro.js") }), el(h3, t`Memoization with memo: Native vs dd<el>`), el("p").append(T` In standard JavaScript applications, optimizing list rendering often involves manual caching or relying on complex virtual DOM diffing algorithms. dd<el>'s ${el("code", "memo")} function provides a simpler, more direct approach: `), el("div", { className: "illustration" }).append( el("div", { className: "comparison" }).append( el("div").append( el("h5", t`Without Memoization`), el(code, { content: ` // Each update to todosArray recreates all elements function renderTodos(todosArray) { return el("ul").append( S.el(todosArray, todos => todos.map(todo=> el("li", { textContent: todo.text, dataState: todo.completed ? "completed" : "", }) )) ); } `, language: "js" }) ), el("div").append( el("h5", t`With dd<el>'s memo`), el(code, { content: ` // With dd<el>’s memoization function renderTodos(todosArray) { return el("ul").append( S.el(todosArray, todos => todos.map(todo=> // Reuses DOM elements when items haven’t changed memo(todo.key, () => el("li", { textContent: todo.text, dataState: todo.completed ? "completed" : "", }) ))) ); } `, language: "js" }) ) ) ), el("p").append(T` The ${el("a", references.memo_docs).append(el("code", "memo"))} function in dd<el> allows you to cache and reuse DOM elements instead of recreating them on every render, which can significantly improve performance for components that render frequently or contain heavy computations. `), el("p").append(T` The memo system is particularly useful for: `), el("ul").append( el("li", t`Lists that update frequently but where most items remain the same`), el("li", t`Components with expensive rendering operations`), el("li", t`Optimizing signal-driven UI updates`) ), el(h3, t`Using memo with Signal Rendering`), el("p").append(T` The most common use case for memoization is within ${el("code", "S.el()")} when rendering lists with ${el("code", "map()")}: `), el(code, { content: ` S.el(todosSignal, todos => el("ul").append( ...todos.map(todo => // Use a unique identifiers memo(todo.id, () => el(TodoItem, todo) )))) `, language: "js" }), el("p").append(T` The ${el("code", "memo")} function in this context: `), el("ol").append( el("li", t`Takes a unique key (todo.id) to identify this item`), el("li", t`Caches the element created by the generator function`), el("li", t`Returns the cached element on subsequent renders if the key remains the same`), el("li", t`Only calls the generator function when rendering an item with a new key`) ), el(example, { src: fileURL("./components/examples/optimization/memo.js") }), el(h3, t`Creating Memoization Scopes`), el("p").append(T` The ${el("code", "memo()")} uses cache store defined via the ${el("code", "memo.scope")} function. That is actually what the ${el("code", "S.el")} is doing under the hood: `), el(code, { content: ` import { memo } from "deka-dom-el"; // Create a memoization scope const renderItem = memo.scope(function(item) { return el().append( el("h3", item.title), el("p", item.description), // Expensive rendering operations... memo(item, ()=> el("div", { className: "expensive-component" })) ); }); // Use the memoized function const items = [/* array of items */]; const container = el("div").append( ...items.map(item => renderItem(item)) ); `, language: "js" }), el("p").append(T` The scope function accepts options to customize its behavior: `), el(code, { content: ` const renderList = memo.scope(function(list) { return list.map(item => memo(item.id, () => el(ItemComponent, item)) ); }, { // Only keep the cache from the most recent render onlyLast: true, // Clear cache when signal is aborted signal: controller.signal }); `, language: "js" }), el("p").append(T` You can use custom memo scope as function in (e. g. ${el("code", "S.el(signal, renderList)")}) and as (Abort) signal use ${el("code", "scope.signal")}. `), el("div", { className: "function-table" }).append( el("dl").append( el("dt", t`onlyLast Option`), el("dd").append(T`Only keeps the cache from the most recent function call, which is useful when the entire collection is replaced. ${el("strong", "This is default behavior of ") .append(el("code", "S.el"))}!`), el("dt", t`signal Option`), el("dd").append(T`An ${el("a", references.mdn_abort).append(el("code", "AbortSignal"))} that will clear the cache when aborted, helping with memory management`) ) ), el(h3, t`Additional Optimization Techniques`), el("h4", t`Minimizing Signal Updates`), el("p").append(T` Signals are efficient, but unnecessary updates can impact performance: `), el("ul").append( el("li", t`For frequently updating values (like scroll position), consider debouncing`), el("li", t`Keep signal computations small and focused`), ), el("h4", t`Optimizing List Rendering`), el("p").append(T` Beyond memoization, consider these approaches for optimizing list rendering: `), el("ul").append( el("li", t`Virtualize long lists to only render visible items`), el("li", t`Use stable, unique keys for list items`), el("li", t`Batch updates to signals that drive large lists`), el("li", t`Consider using a memo scope for the entire list component`) ), el("div", { className: "tip" }).append( el("p").append(T` Memoization works best when your keys are stable and unique. Use IDs or other persistent identifiers rather than array indices, which can change when items are reordered. `), el("p").append(T` Alternatively you can use any “jsonable” value as key, when the primitive values aren’t enough. `) ), el("h4", t`Memory Management`), el("p").append(T` To prevent memory leaks and reduce memory consumption: `), el("ul").append( el("li", t`Clear memo caches when components are removed`), el("li", t`Use AbortSignals to manage memo lifetimes`), el("li", t`Call S.clear() on signals that are no longer needed`), el("li", t`Remove event listeners when elements are removed from the DOM`) ), el("h4", t`Choosing the Right Optimization Approach`), el("p").append(T` While ${el("code", "memo")} is powerful, different scenarios call for different optimization techniques: `), el("div", { className: "function-table" }).append( el("dl").append( el("dt", t`memo`), el("dd").append(T` Best for list rendering where items rarely change or only their properties update. ${el("code", "todos.map(todo => memo(todo.id, () => el(TodoItem, todo)))")} Use when you need to cache and reuse DOM elements to avoid recreating them on every render. `), el("dt", t`Signal computations`), el("dd").append(T` Ideal for derived values that depend on other signals and need to auto-update. ${el("code", "const totalPrice = S(() => items.get().reduce((t, i) => t + i.price, 0))")} Use when calculated values need to stay in sync with changing source data. `), el("dt", t`Debouncing/Throttling`), el("dd").append(T` Essential for high-frequency events (scroll, resize) or rapidly changing input values. ${el("code", "debounce(e => searchQuery.set(e.target.value), 300)")} Use to limit the rate at which expensive operations execute when triggered by fast events. `), el("dt", t`memo.scope`), el("dd").append(T` Useful for using memoization inside any function: ${el("code", "const renderList = memo.scope(items => items.map(...))")}. Use to create isolated memoization contexts that can be cleared or managed independently. `), el("dt", t`Stateful components`), el("dd").append(T` For complex UI components with internal state management. ${el("code", "el(ComplexComponent, { initialState, onChange })")} Use when a component needs to encapsulate and manage its own state and lifecycle. `) ) ), el(h3, t`Known Issues and Limitations`), el("p").append(T` While memoization is a powerful optimization technique, there are some limitations and edge cases to be aware of: `), el("h4", t`Document Fragments and Memoization`), el("p").append(T` One important limitation to understand is how memoization interacts with ${el("a", references.mdn_fragment).append("DocumentFragment")} objects. Functions like ${el("code", "S.el")} internally use DocumentFragment to efficiently handle multiple elements, but this can lead to unexpected behavior with memoization. `), el(code, { content: ` // This pattern can lead to unexpected behavior const memoizedFragment = memo("key", () => { // Creates a DocumentFragment internally return S.el(itemsSignal, items => items.map(item => el("div", item))); }); // After the fragment is appended to the DOM, it becomes empty container.append(memoizedFragment); // On subsequent renders, the cached fragment is empty! container.append(memoizedFragment); // Nothing gets appended `, language: "js" }), el("p").append(T` This happens because a DocumentFragment is emptied when it's appended to the DOM. When the fragment is cached by memo and reused, it's already empty. `), el("div", { className: "tip" }).append( el("h5", t`Solution: Memoize Individual Items`), el(code, { content: ` // Correct approach: memoize the individual items, not the fragment S.el(itemsSignal, items => items.map(item => memo(item.id, () => el("div", item)) )); // Or use a container element instead of relying on a fragment memo("key", () => el("div", { className: "item-container" }).append( S.el(itemsSignal, items => items.map(item => el("div", item))) ) ); `, language: "js" }) ), el("p").append(T` Generally, you should either: `), el("ol").append( el("li", t`Memoize individual items within the collection, not the entire collection result`), el("li", t`Wrap the result in a container element instead of relying on fragment behavior`), el("li", t`Be aware that S.el() and similar functions that return multiple elements are using fragments internally`) ), el("div", { className: "note" }).append( el("p").append(T` This limitation isn't specific to dd<el> but is related to how DocumentFragment works in the DOM. Once a fragment is appended to the DOM, its child nodes are moved from the fragment to the target element, leaving the original fragment empty. `) ), el(h3, t`Performance Debugging`), el("p").append(T` To identify performance bottlenecks in your dd<el> applications: `), el("ol").append( el("li").append(T`Use ${el("a", references.mdn_perf).append("browser performance tools")} to profile rendering times`), el("li", t`Check for excessive signal updates using S.on() listeners with console.log`), el("li", t`Verify memo usage by inspecting cache hit rates`), el("li", t`Look for components that render more frequently than necessary`) ), el("div", { className: "note" }).append( el("p").append(T` For more details on debugging, see the ${el("a", { href: "p07-debugging.html", textContent: "Debugging" })} page. `) ), el(h3, t`Best Practices for Optimized Rendering`), el("ol").append( el("li").append(T` ${el("strong", "Use memo for list items:")} Memoize items in lists, especially when they contain complex components. `), el("li").append(T` ${el("strong", "Clean up with AbortSignals:")} Connect memo caches to component lifecycles using AbortSignals. `), el("li").append(T` ${el("strong", "Profile before optimizing:")} Identify actual bottlenecks before adding optimization. `), el("li").append(T` ${el("strong", "Use derived signals:")} Compute derived values efficiently with signal computations. `), el("li").append(T` ${el("strong", "Avoid memoizing fragments:")} Memoize individual elements or use container elements instead of DocumentFragments. `) ), el(mnemonic), ); }