This is the third post about this year’s work on the Layer-Based SVG Engine (LBSE) in WebKit. In the first part I described conditional layer creation, which fixes the weakest point of the original LBSE design: a RenderLayer for every SVG renderer. In the second part I explained paint order segments, the design that keeps SVG’s document order intact when composited elements sit next to siblings that have no layer of their own. At the end of the second post I promised a dedicated post about the performance work, to follow after my holidays. It took longer than planned, but it is here just in time for my talk “Are We LBSE Yet?” at the WebKit Contributors Meeting 2026.
This post is longer than I usually write, and it contains plenty of statistics. I am aiming to convince you that we take performance measurements seriously and compare both engines in a fair way.
Why Suits?
With LBSE, SVG can take part in the RenderLayer tree, just like HTML/CSS. SVG content can then use hardware-accelerated
compositing, accelerated transform animations, 3D transforms, perspective and z-index. But as I explained in the first
post, none of that matters until LBSE is at least as performant as the legacy SVG engine on regular, non-accelerated SVG
content.
One of the benchmarks we use to measure SVG performance is MotionMark
1.3.1, and in particular its Suits subtest, which I already introduced
in the first post. As a quick reminder, Suits builds a flat <svg> full of clipped <rect> elements and <path>
elements, each with a gradient. A
requestAnimationFrame loop changes
the transform attribute of every shape in each frame, and the score is how many shapes the engine keeps moving at 60
FPS. Suits avoids the compositing fast path, so LBSE brings no advantage there, only potential overhead. On top of that,
LBSE competes against a legacy SVG engine that has been tuned for two decades. No surprise, then, that in April 2026
LBSE scored 26.8% below Legacy on Suits, the largest gap of all the benchmarks we track. That made Suits the focus
of our profiling and optimization work in the following months.
A fair comparison
We run only the Suits test, in MotionMark’s ramp mode with a 60-second ramp. It runs in MiniBrowser, in fullscreen, on an Apple M4 Max with macOS 26.5. For every revision, WebKit is built once, in release mode. LBSE is just a runtime setting, so both engines run the exact same binary. That way LBSE is always compared against Legacy on the same WebKit revision, instead of against a Legacy score that was measured once and then reused. If an improvement lands somewhere in the shared WebCore code, both engines pick it up, so it does not show up as an LBSE win, which is exactly what we want.
For each revision, each engine first gets one warmup run, whose result is thrown away. Then the runs are interleaved: Legacy, LBSE, Legacy, LBSE, Legacy, LBSE. Before every run, all WebKit processes are killed, and background activity on the machine is kept to a minimum. MotionMark reports an 80% confidence interval for every run, and when that interval is ±10% or wider, the ramp did not converge and the score cannot be trusted. Those runs are dropped and repeated, so every data point that we use to compare LBSE with Legacy is the median of three runs that did converge, for both engines at every revision. The next section discusses in detail why the median is a good choice here, and how to assign an uncertainty to each data point derived this way.
Signal or noise?
Almost every conclusion in this post comes down to one question: is a difference between two numbers significant, or could it just be noise? So the size of the noise matters a lot, and deserves an in-depth study and a corresponding statistical analysis, which is a fun topic for a former astroparticle physicist like me.
One run. A single MotionMark run keeps raising the complexity of the scene, and its score is the highest complexity that still renders at 60 FPS. Besides the score, MotionMark computes the 80% confidence interval mentioned above, which says how precisely the ramp pins down that score. Let’s call it . Across more than 150 runs per engine, the median is ±1.1% for LBSE and ±2.4% for Legacy.
One run: the score is the highest complexity still rendered at 60 FPS, and the 80% interval says how precisely the ramp pins it down.
But that interval only describes a single ramp, not how repeatable the score is when you run it again. In a separate noise study, we ran each engine five times on the same build, at several different revisions. We only kept runs that we call “calm”: each one scored close to the other runs of its series, and none landed clearly below its siblings. A run’s own confidence interval cannot tell whether it is a bad run. Only a comparison with the other runs in the series can. The calm runs scatter with a standard deviation of about 1.5% for LBSE and 1.4% for Legacy. That scatter is Gaussian-like, and every Gaussian has 80% of its values within ±1.28σ, just like it has 95% within about ±2σ. Therefore 80% of repeated LBSE runs land within = ±1.28 · ≈ ±1.28 · 1.5% ≈ ±1.9%, and repeated Legacy runs within ±1.8%. Yet a single LBSE run only claims ≈ ±1.1%, so it underestimates how much repeated runs scatter. For Legacy it is the other way around: a single run claims ≈ ±2.4%, which does cover the ±1.8% of repeated runs.
LBSE: 80% of repeated runs land within Imany ≈ ±1.9%, more than the Ione ≈ ±1.1% interval of a single run.
So, and are both 80% ranges, but they answer different questions. only knows the frame-time data of that single run. shows where the runs actually land when you repeat them. It is like a bathroom scale that shows your weight to 0.1 kg, while your weight on different mornings still varies by a kilo. A single run cannot see this noise between runs, but repeating them helps: the mean of N runs shrinks by a factor of .
Using the median. Even runs whose ramp converged may score too low when they are not “calm”. At 322610@main, Legacy scored 8585, 8694 and 7307. The median of those is 8585, but the mean is 8195, dragged down by a single bad run.
Legacy at 322610@main: one bad run pulls the mean down, the median stays with the other two.
To see how much this matters, we looked at about 20 revisions in which nothing performance-relevant landed. We call them “quiet” revisions. This time we kept all runs, not only the “calm” ones from the noise study above. The more runs go into a point, the less the points scatter. For LBSE, a point scatters by 1.55% with one run, 1.34% with two, 1.26% as the mean of three, and 1.19% as the median of three. Legacy goes from 3.89% to 2.83% and 2.34%, and down to 1.52% as the median: its occasional bad runs hurt the mean, but not the median. So the median costs nothing when the runs are clean, and it protects against the bad ones.
How much a point scatters over ~20 “quiet” revisions, depending on how many runs it combines. With two runs, the median equals the mean.
One point. Every point in the comparison plots below is the median of three runs of one engine at one revision. Such a point scatters by 1.19% for LBSE and 1.52% for Legacy. We call this scatter and use it as the uncertainty of every point from here on.
σpoint: how much a point, the median of three runs, scatters across “quiet” revisions, all runs included.
One step. A step is the change in one engine’s score from one revision to the next. Both points fluctuate independently, so a step is noisier than a single point. Independent noise adds up in quadrature:
That is for LBSE and for Legacy. Strictly speaking, neighboring points are slightly correlated, which makes a step a bit less noisy than this. We ignore that for simplicity, so the 95% ranges we derive next are slightly too wide.
The 95% range. So far we have used 80% ranges, but only because MotionMark reports its interval at 80%. To call a step significant, we want a stricter bar: noise alone should only rarely produce a step that large. So from here on we use 95% ranges. Since 95% of the noise stays within ±2σ, the 95% range of a step is :
| Step | Noise (95% range) |
|---|---|
| LBSE, from one revision to the next | ±2 · 1.68% ≈ ±3.36% |
| Legacy, from one revision to the next | ±2 · 2.15% ≈ ±4.30% |
On its own, a step between two revisions that is smaller than this cannot be told apart from noise. Averaging can improve the sensitivity, as the gap plot further down shows. In the score plots, the shaded band around each line shows the 95% range of one point, ±2 · .
April → October 2026
With all the ingredients in place, and a solid understanding of the statistical uncertainties, we can now look at what six months of work achieved. In October 2026, we essentially reached performance parity with the legacy SVG engine on Suits. The rest of this post discusses in detail which changes were necessary to reach that goal, but let’s first look at the results.
Suits scores of LBSE (orange) and Legacy (blue), April to October 2026. Shaded: the 95% range of one point, ±2.38% for LBSE and ±3.04% for Legacy.
The plot shows the Suits score of both engines for 62 WebKit revisions between April 17 and October 5, 2026. LBSE, the orange line, went from 5775 points in April to 8682 points in October, an improvement of +50.3%. That is a lot more than the 26.8% gap we started with, and there are two reasons for it. The gap is measured against the Legacy score, while the improvement is measured against LBSE’s own, lower score, so closing a 26.8% gap already takes an improvement of 7888 / 5775 ≈ +37%. And Legacy got faster as well: it went from 7888 points to 8736 points, +10.8%, most of it from a single commit on August 21 that made it 6.5% faster (more on that below). So LBSE not only had to close the gap, but also had to catch up with what Legacy gained on top. Now let’s look at the changes that made this possible.
The patches that stand out
There was no single magic patch, but many different areas in LBSE that needed improvement. The plot below shows how the LBSE score changed from one measured revision to the next. Green columns are steps where LBSE got faster, red ones where it got slower, and everything inside the gray band is within the ±3.36% noise of one step. The black marks show how the legacy SVG engine changed between the same two revisions. When both engines move by a similar amount, that is either noise that hit both alike or a WebKit change that helped both, not an LBSE patch. Legacy is noisier, so a black mark only stands for a significant change when it lies outside Legacy’s own noise range of one step (±4.30%, see the table above). Two of them do, and both are circled: a genuine, legacy-only improvement of +6.5% in August, and a fluke of −5.9% in September.
Each column is the change in the LBSE score from the previous measured revision. The black marks show how the legacy SVG engine changed between the same two revisions. Labeled: the LBSE steps outside the ±3.36% noise. The red circles mark the two Legacy steps outside Legacy’s own ±4.30% range.
Six LBSE steps are outside the ±3.36% noise. Five of them are patches, the bold ones in the table below. The sixth, +3.5% on September 30, is not: no SVG change landed there. It only shows up because the revision before it (September 29, 72 commits earlier) happened to score unusually low.
This table lists the LBSE optimization patches that affect Suits, together with the LBSE step measured at its revision:
| When | Revision | Patch | Step |
|---|---|---|---|
| Apr 21 | 311726@main | Suppress LayoutRepainter in hidden and anonymous containers | +1.4% |
| Apr 27 | 312090@main | Cache the local transform of non-layered elements | +1.3% |
| May 19 | 313491@main | Skip the redundant fill clip for rects clipped inside their bounds (from Legacy) | +4.7% |
| Jun 3 | 314458@main | Defer per-element transform attribute work (one flush per frame) | +2.5% |
| Jul 2 | 316428@main | Cache the resolved fill / stroke paint server | +4.6% |
| Jul 6 | 316543@main | Cache the concatenated transform attribute matrix | +4.8% |
| Jul 6 | 316545@main | Cache the viewport size for the transform reference box | +1.0% |
| Jul 10 | 316880@main | Coalesce the transform flush into one repaint per container | +2.6% |
| Jul 13 | 317077@main | Cache the transform origin | +1.4% |
| Aug 1 | 318375@main | No RenderLayer for SVG clip-path | +8.9% |
| Aug 2 | 318410@main | Skip the outline paint pass for outline-free renderers | +5.6% |
| Aug 3 | 318440@main | Skip the per-frame visual-overflow recompute when culling | +1.8% |
| Aug 10 | 318882@main | Cache the paint server on the renderer | +2.9% |
| Aug 18 | 319390@main | No RenderLayer for SVG mask (Suits has no masks) | +0.8% |
| Oct 5 | 322676@main | Avoid redundant save/restore pairs (on average 8.5 → 6.5 IPC messages per shape) | +0.9% |
Steps that are not marked bold are within the ±3.36% noise of a single step. They still add up, but each of them on its own cannot be told apart from noise. Therefore we only look at the most impactful patches here, and discuss those in more detail.
The first high-impact patch landed in May, and it borrowed an optimization from Legacy. When a rect is clipped by a
<clipPath> whose bounds lie inside the rect anyway, the extra clip for the fill is redundant. Skipping that clip in
LBSE as well was worth +4.7%.
In July, the focus shifted to caching results that LBSE recomputed more often than necessary. Every shape in Suits is
filled with a gradient, which SVG calls a paint server. Every time LBSE painted a shape, it looked up that gradient
again from the fill and stroke properties. Caching the resolved paint server raised the score by +4.6%. The
second July patch dealt with the transforms that Suits animates. Each shape’s transform attribute is a list of
transform functions, such as translate(…) rotate(…), and multiplying them together yields a single matrix. LBSE used
to perform that multiplication up to three times per frame for every shape, although the result only changes when the
attribute does. Caching that matrix added another +4.8%.
August brought the biggest jump, and it completed work that had started in June. Conditional layer
creation, the topic of the first post, removed the RenderLayers of the paths in
Suits on June 22, but that did not help on its own, because every clipped rect still needed a layer to be able to apply
its clip-path. Making clip-path layer-free, as announced at the end of the second
post, was the single largest step, +8.9%. The clip setup now lives in a
shared ClipPathPaintScope, which is used by two code paths: CSS boxes and layered renderers go through
RenderLayer::setupClipPath(), while LBSE’s layer-free SVG shapes use it directly during painting. Since then, the only
layer left in Suits is the one for the <svg> root.
With the layers gone, smaller per-shape costs started to show up in the profiles. One of them was the outline paint pass: every SVG child went through it, although most renderers have no outline, so for them the pass did nothing at all. Skipping it for outline-free renderers brought another +5.6%, and from early August on, LBSE was ahead of Legacy.
The gap over time
The patches show what made LBSE faster, but the question this whole effort is really about is a different one: how far behind Legacy is LBSE, or is it even ahead? To answer that, we look at the gap: at every revision, the LBSE score relative to the Legacy score of the same build, in percent.
LBSE relative to Legacy: above zero, LBSE is faster. Gray: the ±2.7% noise range of a single gap value.
Dashed: the averages before and after August 21, with their ±0.8% range in green.
How precise is the gap? Both engines run interleaved on the same build, so the part of the noise they share cancels in the gap, and so does any change that affects both alike. Let be how much the gap scatters around the two dashed averages in the plot. We determined it with a noise model, whose details we omit here for brevity, and it comes out at about 1.35%, less than the that two independent engines would give. So the noise of a single gap value stays within about ±2.7% in 95% of cases, as visualized by the gray band in the plot. The average of about 20 revisions is known much better, to ±0.8% (the noise model accounts for correlations between neighboring revisions), shown by the green bands. That precision is the reason for building this model in the first place: it lets us say, in a statistically solid way, whether LBSE is ahead of or behind Legacy, even when the difference is only about 1%.
Armed with a good understanding of its uncertainty, we can now analyze the gap over time precisely. The gap closed steadily from April on and crossed zero in early August. From early August to August 21, LBSE was ahead of Legacy, on average by 2.0%, with a best margin of +4.8% on August 21, right before the commit discussed below. Then a single commit made Legacy 6.5% faster, which put it back in the lead. Over the 18 revisions from August 21 to October 3, before the latest LBSE patch, LBSE was behind by 1.4% on average. Both averages are known to ±0.8%, so both are significant: LBSE really was ahead in August, and it was slightly behind afterwards.
Why the August lead went away
When I first saw the gap turn negative again in late August, I assumed we had regressed. Measuring many more revisions around that point narrowed it down to a single commit.
The gap from August 18 to 24, one point per measured revision, with vertical lines between days.
It drops at a single commit, 319635@main: Legacy gained 6.5%, LBSE stayed within the noise.
WebKit paints through a GraphicsContext, which holds the current drawing state, such as the transform, the clip and
the fill and stroke settings. Painting code that temporarily changes this state wraps the change in a save() and a
restore(), which push the state onto a stack and pop it off again. In commit
319635@main, such a save/restore pair around the SVG gradient fill and stroke
was removed, because it was redundant. Since WebKit on macOS paints in the GPU process, each save and each restore is an
IPC message from the WebContent process to the GPU process, so the pair cost two messages per gradient-filled shape and
frame, and Suits fills every shape with a gradient.
Legacy jumped by +6.5% with this one commit. LBSE stayed within the noise, because LBSE never sent those messages in the first place. So this commit removed a handicap from Legacy rather than giving it an advantage, and LBSE’s lead in August partly came from that handicap.
A closer look at save/restore pairs revealed that LBSE had redundant ones of its own, around the painting of transformed shapes. The most recent patch, 322676@main from October 5, avoids them and takes LBSE from 8.5 to 6.5 IPC messages per shape on average. It moved LBSE by +0.9% and brought the gap to −0.6% at the latest revision. That is within the noise of one step, but it points in the right direction.
How close did we get?
Let’s end the analysis with a closer look at the weeks since August 21.
Both engines since August 21, shaded by the 95% range of each point. The last point includes the save/restore fix.
In these weeks, the two lines run close together, and their shaded bands overlap everywhere. They also tend to move together: in the dip in late September, for example, both engines drop, although Legacy drops more. The part they share cancels in the gap, and only the rest shows up there. Legacy stays slightly on top in most of these weeks, which matches the −1.4% average from the gap plot. At the last point, after the save/restore fix for transformed shapes in LBSE, the two engines are only 54 points apart: 8682 for LBSE and 8736 for Legacy.
The following table sums up how far each engine has come since April:
| Metric | Value |
|---|---|
| LBSE since April | 5775 → 8682 points, +50.3% |
| Legacy since April | 7888 → 8736 points, +10.8% |
| Gap in April | −26.8% |
| Gap August 21 – October 3 | −1.4% ± 0.8%, average of 18 revisions, before the last fix |
| Gap at the latest revision | −0.6%, after the last fix (LBSE +0.9%) |
So in six months, the gap shrank from −26.8% to about −1%. The latest revision, at −0.6%, is consistent with parity, but as a single gap value, its 95% noise range is ±2.7%, far too wide to pin down a gap below 1%. In a few weeks, after landing a few more optimizations that are still in the pipeline, we will take another close look at the data. My gut feeling says it will confirm that LBSE has reached parity, or is even slightly ahead.
A nice side effect: steadier frames
Before we close the performance analysis, there is one more metric worth mentioning, and here LBSE is already clearly ahead of Legacy: the median confidence interval of a single MotionMark run. It is ±1.1% for LBSE, less than half of the ±2.4% for Legacy.
Remember that says how precisely one ramp pins down its score. It gets wide when the frame times jump around during the ramp, so a narrow interval means steady frames. LBSE renders Suits with much more stable frame timing than Legacy. The runs that had to be dropped because their ramp did not converge tell the same story: 69 of the 71 were Legacy runs.
The confidence interval MotionMark reports for each run that feeds the plots, as a percentage of the score. Lower means a steadier run. Runs at or above the dashed ±10% line were dropped.
Apart from two revisions in late April, the LBSE line stays flat at around 1% from May on, while Legacy keeps jumping between about 1.5% and 6%, with its largest spikes from late August on.
Beyond performance: features & fixes
Besides performance, there was a second gap between LBSE and Legacy: correctness. When the LBSE work restarted in
January 2026, after 17 months without any testing, text did not render at all (fixed in
305312@main), most test baselines were outdated, and there were Legacy fixes
that had never reached LBSE. Since then, the expected test failures under LBSE have gone down from 313 to 49, and
the expected crashes from 3 to 0. Of the 136 LBSE patches that landed in 2026, about 100 are fixes like these: crashes,
text, filters, clipping, masking, the outermost <svg> and more. The full
list is on GitHub.
Are we LBSE yet? Almost!
The status of LBSE in October 2026, in a nutshell:
- Composition: layer-free by default. Still to be investigated: opacity, filters and blend modes, which force a RenderLayer even on leaves.
- Performance: −0.6% on Suits at the latest revision, pending confirmation. An A/B test by Apple in mid-August found LBSE performance-neutral on Speedometer 3 and MotionMark 1.3.1. To be repeated.
- Feature parity: 49 expected failures, 29 of them in filters, but also dozens of progressions over Legacy, for
example in
<foreignObject>handling, fixing design issues in Legacy that are more than 15 years old. - Default: not yet. That needs independent verification and the green light from Apple.
Near-parity on Suits is a milestone: it shows the potential of LBSE to compete with the legacy SVG engine, while
providing many exciting new features. Until now, choosing LBSE meant accepting a performance cost in exchange for
hardware-accelerated animations, z-index, 3D transforms and a code path shared with HTML/CSS. On Suits, which
deliberately avoids the accelerated path and is the scenario where LBSE has the least to offer, that cost is now
essentially gone, and we are really excited about it.
The road to default
Here is what we still need before LBSE can become the default:
- Tests: zero regressions, both in WPT and in our own tests.
- Performance: independent verification by Apple across benchmarks and devices.
- Coverage: an LBSE-enabled bot, instead of running the LBSE tests manually, so LBSE never breaks silently again.
- Layers: extend the layer-free approach so that leaves ideally never need a RenderLayer.
Let me close with a huge thanks to Rob Buis, who reviewed most of these patches, and to Simon Fraser, Said Abou-Hallawa, Karl Dubost and Ahmad Saleem for the reviews and the hard questions. And of course thanks to Igalia for funding this round of work. If a fast, hardware-accelerated SVG engine sounds useful to you, we would love to hear from you!
As I said at the beginning, this was a long post with a lot of statistics. I hope I did not bore you, and that the numbers convinced you that both engines got a fair comparison. Thanks for reading to the end!





