Overview of 984: How to Make a DOM Library Render Anything w/ Paolo Ricciuti (Syntax)
This episode of Syntax (hosts Wes Bos & Scott Tolinski) features Paolo Ricciuti (Svelte maintainer and lead of the Svelte team at Mainmatter) discussing Svelte custom renderers — what they are, why the Svelte team is building them, the technical challenges, the approaches taken, and the current status/timeline. The conversation contrasts Svelte’s compiled DOM-focused model with React’s reconciler/virtual-DOM approach, explains the work needed to make Svelte render to non-DOM targets (terminals, native UIs, WebGL, etc.), and describes Paolo’s proof-of-concept work (notably for Lynx).
Key takeaways
- Svelte historically targets the browser by compiling components into direct DOM-manipulating JS. That differs from React’s virtual-DOM + renderer model which makes multi-target rendering easier.
- To let Svelte render to other targets (terminals, native, WebGL), Svelte needs a pluggable "custom renderer" API that delegates element creation/updating/removal and event handling to user-provided adapters.
- Two main design approaches were considered:
- Shim/mock a DOM API around custom renderer objects (lots of DOM quirks; messy).
- Swap/branch the internal runtime API Svelte compiles against so it calls a renderer-specific runtime (Paolo’s preferred approach).
- Practical changes already made: Svelte compiler can emit a structured fragment representation (instead of innerHTML/template strings) — this helps avoid CSP/innerHTML problems and is essential for custom renderers.
- Some features (hydration, certain DOM-specific bind behaviors) don’t map to non-DOM targets and will be disabled or require special handling in custom-rendered apps.
- Paolo built POCs: a basic Lynx renderer (to-do app), and a simple terminal renderer demonstrating reactive updates.
- The project is substantial and currently seeking sponsorship/funding to finish; Syntax (the podcast/app) is sponsoring some work.
Topics discussed
- How React uses a diffing reconciler and separate renderers (React DOM, React Native, Ink) to enable different platforms.
- How Svelte compiles components into DOM operations (showing compiled JS via the Svelte playground / VS Code command).
- The template.innerHTML trick Svelte used to create DOM nodes from compiled HTML strings and why that blocks rendering to non-DOM targets.
- The new compiler option that emits structured fragments (arrays of element shapes) instead of template strings.
- Two renderer strategies: DOM-shim vs separate/custom runtime import.
- Practical demos: Lynx renderer POC, terminal renderer POC, support for CSS in Lynx via Lynx’s CSS support.
- Limitations (bindings, hydration, CSS translation work, many DOM edge-cases).
- Community/funding model for finishing work; where to contact/sponsor.
Technical deep dive / notable insights
- Svelte’s compiled output is highly readable in the playground. Svelte components compile to JS functions that directly call DOM APIs (e.g., createElement, setAttribute, setText).
- Previously Svelte used template.innerHTML to convert the static HTML portion into real DOM nodes. That approach:
- Is fast in the browser, but
- Breaks CSPs and is impossible on non-DOM targets.
- The compiler now supports emitting a fragments representation (an array/object tree describing nodes + props) so runtimes can instantiate nodes without parsing HTML strings.
- Two implementation paths:
- DOM shim: wrap custom-renderer nodes in objects that mimic DOM APIs (but the DOM has many subtle, performant quirks making this brittle).
- Runtime swap/branch: compile Svelte to import a runtime module (SvelteInternalClient vs SvelteInternalCustom) and implement a renderer runtime that provides the lower-level APIs Svelte expects. Paolo initially built a custom runtime POC and a Lynx renderer.
- Hydration is browser-specific — custom renderers can skip hydration logic and therefore simplify the runtime.
- CSS in many non-web targets: Lynx supports a good subset of CSS so component-scoped styles can largely be preserved; other platforms will need CSS translation logic or platform-specific mappings.
- Binding behaviors (e.g., bind:value) are DOM-centric; some bindings may be disabled or converted to event/listener patterns for custom targets.
Challenges & trade-offs
- DOM is messy and full of quirks used for performance (e.g., textContent = '' clears children). Shimming these is error-prone and inefficient.
- Maintaining two separate runtimes (client + custom) risks code duplication and ongoing maintenance burden. Paolo is moving toward a single runtime with branching for DOM shortcuts vs renderer calls.
- Some Svelte features simply don’t make sense outside the web (hydration), so mapping Svelte’s full feature set across all renderers is not feasible — trade-offs are needed.
- Funding/time: the work is large; Paolo worked full-time for a few months under sponsorship and now seeks more sponsors to finish.
Current status & timeline (practical status)
- Paolo built POCs: fragments compiler output, a minimal custom runtime, a Lynx renderer POC (to-do app), and a basic terminal renderer.
- The plan is to converge on a single runtime with branching to avoid duplicated maintenance while still supporting DOM-optimized shortcuts.
- No firm release date. Progress depends on more sponsorship/funding and continued contributor time.
- How to help/sponsor: visit svelte-custom-renderers.com to get in touch and explore sponsoring the effort. Syntax.fm indicated they will sponsor part of the work.
Action items / How you can engage
- If your company needs Svelte-native or alternate-target apps (native, WebGL, terminal, etc.), consider sponsoring the custom renderers effort (svelte-custom-renderers.com).
- Try the Svelte Playground and use the "JS output" tab and VS Code command ("Show compiled code") to learn how Svelte compiles components.
- If you can contribute code or time: follow the Svelte repo and the custom-renderers discussions; review the fragments output and the runtime POC when available.
- For Lynx/React Native-style projects, consider whether a renderer for your platform would be useful—this can justify sponsorship.
Notable quotes
- “Svelte does not use a virtual DOM — it compiles to direct DOM operations.” — Paolo
- “We had to change the compiler so it can output fragments (an array describing elements) rather than template.innerHTML strings — that was a required step.” — Paolo
- “The DOM is just a mess… there are so many quirks.” — Paolo, explaining why a DOM-shim approach is brittle.
Picks & plugs (from Paolo)
- Sick pick: OpenCode — Paolo uses it as his main AI tool because it’s configurable and model-agnostic.
- Plugs:
- svelte-custom-renderers.com — to sponsor or contact the team working on custom renderers.
- Mainmatter blog: Paolo’s post “Why I choose (and continue to choose) Svelte” (mainmatter.com blog).
- Paolo’s TMCP project — an alternative TypeScript Minecraft server SDK (lightweight, modular).
- Valibot — Paolo’s preferred schema/validation library for small bundle size.
Resources mentioned
- MadCSS tournament: madcss.com (promo at episode start)
- Svelte Playground (JS output / source maps)
- Svelte compiled-output command in VS Code (Show compiled code)
- svelte-custom-renderers.com — contact/sponsor page for this initiative
- Mainmatter blog — Paolo’s article on why he uses Svelte
- Lynx (framework-agnostic/native alternative referenced in discussion)
- Ink (React terminal UI renderer), React Native, Raycast (examples of non-browser React renderers)
- Sentry sponsor mention: sentry.io/syntax (ad for episode)
If you want the most important single follow-up: check the Svelte Playground’s JS output to see how Svelte compiles to DOM calls, and if your team needs native or non-DOM targets for Svelte, consider reaching out via svelte-custom-renderers.com to sponsor the work.
