Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 1,800 words · 5 segments analyzed
The last time I wrote here about the layer based SVG engine (LBSE) was back in autumn 2021, when I published the technical design document. A lot has happened since then, but maybe not what you would expect after almost four years. The engine landed in WebKit and it works well, but it is still not the default. You have to switch it on by toggling a runtime setting (in MiniBrowser). Between autumn 2021 and late 2022 LBSE was bootstrapped upstream, patch after patch. We added support for all the individual building blocks that make up SVG: paths, shapes, text, polygons, etc. within the new LBSE design. After that first big push the work stalled for a few months and resumed in mid-2023, and lasted until April 2024, due to generous support by Wix. During that period most advanced painting features, such as clipping, masking, filters, non-solid paint servers (patterns, gradients), markers, etc. were all implemented, sharing the logic with HTML and CSS rather than running through the separate code paths, as the legacy SVG engine did. Then the work paused again. A project of this size needs sustained funding to move forward, and for a while that funding was not there.LBSE is promising, but it still has to earn its place, and it is worth being precise about what that means, because the goal was never simply “make SVG faster”. The legacy SVG engine is an island: historically grown, it has its own painting code, its own handling of transforms, clipping, masking, etc. and it shares only a little of the code that renders HTML and CSS for historical reasons. Many improvements that went into WebKit, went into “the other engine”, the main part, the HTML/CSS rendering engine. GPU-accelerated transforms and animations, accelerated compositing, all the work that made HTML/CSS fast. SVG sat right next to it and got none of it. That is what LBSE is really about. Put SVG on the same machinery, and it inherits all of that at once, and it keeps inheriting whatever comes next, instead of someone having to build it a second time for SVG alone.The catch is that shared machinery is general machinery, and generality is not free.
The legacy engine has been hand tuned for exactly one job, with two decades of tuning behind it. LBSE, on the other hand, has to integrate surgically into the shared HTML/CSS rendering code, without regressing that code even a little. This is the hottest code in the engine and it renders every web page out there, so sprinkling SVG special cases through layout and painting until LBSE looks good is simply not an option. Whatever LBSE needs either fits the existing design, or it has to make the existing design better for HTML and CSS too.There is a second kind of work hiding in there as well. Some things that are rare in HTML are everywhere in SVG. Transformations are the obvious example. A typical web page has a handful of transformed elements, while in SVG almost every single element can carry a transform, and deeply nested transforms are the norm rather than the exception. The shared code was written with HTML in mind, and it does its job perfectly well there. So parts of the shared machinery have to be reworked, and fast paths that nobody ever needed for HTML have to be invented from scratch, without making anything slower for HTML in the process.So the bar is a double one. LBSE first has to be competitive with the legacy engine on the plain, everyday rendering that legacy is already good at, because nobody switches engines if a static icon that never moves suddenly takes longer to paint. The rest, the hardware acceleration and every future improvement to the HTML/CSS engine that SVG now gets for free, only counts as a win once that first bar is met.Fast forward to early 2026: This year the status quo finally changed. Igalia made a one-off investment into a fresh round of LBSE development, to give the engine the chance to prove itself. We are hopeful to finally show the world that LBSE is performant, and to attract new partners who share our vision that a performant, hardware-accelerated SVG engine is the future for UIs on embedded devices. This is the first of two posts covering the first half of 2026, where we stand now and how the LBSE project evolved since the long break in 2024.
The last six months were intense, and ended with the single most invasive change to LBSE since it was first written. This post covers that change, and the next one picks up the hard problem it left behind.A quick reminder of where we wereWhen we designed LBSE, the central idea was simple. Instead of keeping the old, separate SVG painting code, SVG should reuse the very same machinery that HTML and CSS already use, the RenderLayer tree. That is what unlocked hardware-accelerated transform animations, perspective transformations, z-index support - all the nice things SVG never had before in WebKit.To get there quickly, we took a shortcut in the early days. Every single SVG renderer got its own RenderLayer. A <rect>, a <path>, a <g>, everything. The reason was convenience: if every element has a layer, then the existing layer tree already knows how to order children, apply transforms, clip and composite. We did not have to reinvent any of that. Furthermore, the intrinsic SVG paint order, which follows the DOM structure 1:1, is guaranteed automatically this way, while z-index support is still there to deviate from that order when desired. All of that just works out of the box once every renderer receives its own layer. RenderSVGModelObject::requiresLayer() simply returned true all the time, and the rest of WebCore did the heavy lifting.We knew from the start that this approach would most likely never ship in this form, because it is simply too wasteful. But that was fine. It was never meant to be the final design. What it gave us was a way to finally try out the things SVG had been missing for years, all running through the same machinery as HTML and CSS. It also gave us the possibility to reimplement all SVG render tree classes, in a way that’s fully unified between HTML and SVG, so that e.g. an SVG <clipPath> applied to an SVG element or an HTML element would no longer run through different code paths, as it historically did in WebKit. Thus just maintaining a 1:1 correspondence between renderers and layers was the fastest path to seeing the idea actually work, and once we had, the real shape of the engine could follow.
Correctness firstWhen we resumed work on LBSE, we first had to make sure the engine worked again properly after the long pause of upstream work. LBSE received no testing during that period, so it was likely to be in a broken state, and that turned out to be the case. Text rendering was initially fully broken, most testing baselines were outdated, many new regressions were present, where the legacy SVG engine received fixes but LBSE did not. It took us a while to recover.Rob Buis, my partner in the endeavor to bootstrap LBSE, did a lot of this work, so I could focus on attacking the remaining performance problems, profiling the engine, and trying new designs. We fixed a pile of crashes, for example in getBBox on <use> elements, in paths with NaN coordinates and line caps, and in markers whose children change. We fixed real rendering bugs too, including text not rendering at all, text opacity, gradients with a non-invertible gradientTransform, and support for SVG view fragments.Filters got a lot of attention through spring. We made invalid filters behave like they do for HTML instead of dropping the element entirely, fixed feOffset clipping, filter scaling, and a convolution filter test. The most important filter change, though, has not landed yet. Once conditional layer creation, the change the rest of this post is about, removes the intermediate layers, a filter’s coordinate space no longer lines up with what the painting code assumed, and a lot of filtered content ends up in the wrong place. A pending pull request corrects that for non-layered renderers and clears a large batch of filter test failures under LBSE once conditional layers are enabled.None of this is glamorous, but it is the foundation. With the engine behaving well again, we could finally go after the thing this whole effort was about.The real goal, performanceThe point of LBSE was always performance. It was designed to unlock hardware-accelerated animations, which can potentially run much faster than in the legacy engine, since re-layouts and re-rasterization can be skipped entirely as the compositor is responsible for applying those transformations to the already-rasterized content. The catch is that scenarios which deliberately avoid that fast path suffer instead from the overhead of a layer per element.
The benchmark we measure ourselves against here is MotionMark, and in particular its Suits subtest. Suits adds a large number of SVG primitives (rects and paths) that are clipped via <clipPath> elements, and animates them by modifying the transform attribute of each shape programmatically, in a requestAnimationFrame driven setup. It measures how many of them the browser can keep moving smoothly, which makes it a demanding, honest test of the SVG pipeline.And here our early shortcut came back to bite us. Remember, every SVG renderer had its own RenderLayer. On a scene with thousands of shapes, that means thousands of layers. Each layer costs memory, and worse, each layer takes part in a lot of per-frame bookkeeping. Position updates, z-order lists, transform updates, repaint walks. None of these shapes should actually need a layer - a path/rect with a plain 2D transform is the most ordinary thing an SVG can contain. Yet we paid the full layer price for every one of them.So the way forward was clear: stop giving every renderer a layer, and create one only when an element actually needs it. The first focus is transformed elements, the plain shapes that carry nothing but a 2D transform. Clipped elements keep their layer for now, and dropping that one too is future work. In Suits this first step already removes the layer from every transformed path, leaving only the clipped rects and the <svg> root, as the diagram below illustrates.Old: a layer for everythingNew: no layers for transformed elements<svg>defsgrad + clips<rect>clip-pathtransform<path>transform<rect>clip-pathtransform<path>transform…LLLLLone RenderLayer per shape, thousands of them<svg>defsgrad + clips<rect>clip-pathtransform<path>transform<rect>clip-pathtransform<path>transform…LLLlayers only for the clipped rects and the <svg> rootSuits builds a flat list of shapes directly under the <svg> root: alternating clipped <rect> elements and plain <path> elements, each with a gradient fill and a transform that is updated on every animation frame. A blue L marks a renderer that owns a RenderLayer.