Building a Dual-Renderer Canvas: Lessons Learned

Why Lega renders every design through both a Canvas2D backend and a legacy SVG fallback from a single compiled scene, and what we learned building it without letting the two drift apart.

Cover Image for Building a Dual-Renderer Canvas: Lessons Learned

Lega's editor can render a document two different ways: through a Canvas2D scene backend, or through a legacy SVG-based DOM renderer. A single environment flag, NEXT_PUBLIC_EDITOR_RENDERER, picks between canvas and dom. Most users only ever see the canvas path. But the fact that a second, complete rendering backend exists, stays correct, and can be switched to at any time is not a leftover from a migration we forgot to clean up. It is a deliberate architectural decision, and it taught us some things worth writing down.

Why Have Two Renderers at All

Lega's editor started life as a DOM/SVG-based canvas. That approach has real advantages: you get the browser's layout and hit-testing engine for free, styling is CSS, and debugging is "open devtools and look at the element." It also has a ceiling. Complex scenes with many layers, filters, clip masks, and procedural decor generators get slow in the DOM in ways that are hard to claw back without fighting the browser's own rendering pipeline.

The move to a Canvas2D scene backend was about performance and control: drawing directly to a canvas element gives you a predictable cost model and headroom that DOM composition does not. But rewriting the renderer is not free, and a rewrite of this scope touches every layer type, every interaction mode, and every export path in the product. Shipping it as a flag-switched dual system, rather than a flag-day cutover, was the only way to de-risk that migration for a live product.

The Rule That Made This Safe: One Compiler, Two Backends

The single decision that made a dual renderer tractable rather than a maintenance nightmare is this: there is exactly one scene compiler, and both backends consume its output. Layer data, styles, transforms, and layout are all resolved into an intermediate scene representation once. The Canvas2D backend and the SVG backend each know how to draw that scene representation, but neither backend does its own interpretation of raw layer data.

This matters because the alternative, letting each backend independently interpret layer models, is how dual-renderer systems rot. Without a shared compiled scene, it is only a matter of time before a feature gets implemented against one backend and quietly never reaches the other. A gradient direction gets computed slightly differently. A clip mask edge case works in canvas and silently fails in SVG. Users on different renderer flags see different documents, and nobody notices until a bug report arrives that cannot be reproduced.

With one compiler, that class of bug cannot happen by construction, because there is no code path where a backend sees anything other than the fully resolved scene. If the compiler is correct, both backends draw the same thing. If it is wrong, both backends are wrong identically, which is a far easier bug to catch and fix than a divergence between two independently-correct-looking renderers.

Export and Preview Ride the Same Path as the Live View

A second lesson, learned the harder way, was to resist the temptation to build a separate, "good enough" rendering path for exports and thumbnails. It is tempting: exports do not need to be interactive, so why not use a simpler renderer just for that? Because "simpler" is exactly how view and export drift apart. A design that looks right in the editor and wrong in the exported PNG is one of the most damaging bugs a design tool can ship, because users only discover it after they have already shared the file.

Lega's export and thumbnail generation always render through the Canvas2D backend, using the same draw calls as the live canvas view. There is no second, export-specific rendering implementation to keep in sync. What you see in the editor is, mechanically, what gets exported. This constraint costs some flexibility, you cannot special-case export rendering for convenience, but it buys back an entire category of trust-eroding bugs.

What Broke Along the Way, Honestly

A dual-renderer migration surfaces bugs that a single-renderer system never would, because it forces you to define behavior precisely enough that two independent drawing implementations agree on it. Malformed data that a DOM renderer would silently absorb (a slightly out-of-range value, an unexpected layer shape from an AI-generated layout) needs to be normalized before it reaches either backend, not tolerated inconsistently by each one. We ended up pushing several rounds of layout normalization earlier in the pipeline specifically so malformed layers, including AI-generated ones, can never reach either renderer in a shape that only one of the two backends knows how to handle.

The lesson generalizes: a dual-backend system is a forcing function for correctness. Ambiguity that a single, forgiving implementation could paper over has nowhere to hide when two backends have to agree.

What This Means for You as a User

In practice, almost nobody needs to think about which renderer is active. The canvas backend is the default and the one actively developed forward; the SVG path exists as a compatibility fallback. We cover the user-facing implications, including when the distinction might actually matter to you, in Canvas vs Legacy Renderer.

Why This Is Part of a Bigger Architectural Pattern

The dual renderer is not an isolated decision. It follows the same instinct that shapes the rest of Lega's editor core: put one authoritative representation in the middle (a compiled scene here, a command-driven state tree in Legon's plugin architecture) and make every consumer, whether that is a rendering backend, a human editing session, or an AI-generated layout, go through it rather than inventing its own interpretation. We cover that pattern in more depth in How Legon's Plugin Architecture Works and How Lega's AI Adapter System Works. For the full architectural picture, see Editor Architecture Overview.

If there is one takeaway from building this system, it is that supporting two renderers safely has almost nothing to do with the renderers themselves, and almost everything to do with disciplined boundaries around what each one is allowed to see.