Skip to content

feat: Replace yoga with taffy - #3572

Open
carlobeltrame wants to merge 11 commits into
masterfrom
worktree-taffy-layout
Open

carlobeltrame wants to merge 11 commits into
masterfrom
worktree-taffy-layout

Conversation

@carlobeltrame

@carlobeltrame carlobeltrame commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2542
Closes #3349

The commit "fix(layout): do not resolve node styles twice on page relayout" also resolves the following lineHeight issues (became necessary with the taffy migration, but could probably be cherry-picked while we are still on yoga):
Fixes #3566
Fixes #3083
Fixes #3044
Fixes #3402

Why

yoga-layout has stopped publishing releases on npm 2 years ago. There are a number of bugs and weird edge case behaviours that either are not going to be fixed because yoga isn't developed very actively anymore, or because they would break old applications relying on the buggy behaviour. Yoga also implements only flexbox, and implementation of other CSS modes is progressing only very slowly. Even if it would be implemented, it is questionable whether this would become available in the yoga-layout package at all, and if it would, whether it will be a massive breaking change requiring react-pdf to adapt or pass on the breaking changes to users.

taffy was originally started as a rust rewrite of yoga, but has grown to support much more than flexbox, including grid and block layout with floats. taffy is currently very actively maintained, and the taffy-layout npm package (WASM bindings published by an independent contributor) got its last release 2 weeks after the taffy release. It powers a growing number of projects with similar size and goals as react-pdf, e.g. for rendering react components to images, or for terminal UIs.

The taffy maintainer @nicoburns monitors yoga's development, provides taffy's test fixtures to yoga, and uses yoga's test fixtures in taffy as well, so I expect compatibility to be very high. He also posts comments regularly in many open source projects to improve awareness of taffy and yoga and their features, and has commented a few times in react-pdf issues before.

This PR is intended to get the discussion rolling again on whether react-pdf should replace yoga with taffy. It doesn't take advantage of taffy-only features like grid yet, but aims to simply replace yoga with taffy in the project.

Reviewing

Most of the changed files are just removal of the omnipresent yoga parameter threaded through lots of functions. Once you mentally check these off, this seems like a very reasonable amount of code change for this big a technical debt fix IMO. But I'm waiting to see what you think @diegomura

Compatibility, tests in a real app

This implementation is almost entirely AI-generated using Claude Fable 5.1. However, I tested it in a real app of mine with rather complex pdfs, and fixed the few issues that were found this way. Ultimately, there was only a one-line style change needed on the application side, everything else was fixed in this PR itself, in the integration of react-pdf and taffy.

Changes for react-pdf users compared to yoga

There are some changes in behaviour between yoga and taffy, most of which boil down to taffy following the CSS spec more closely. Some defaults were introduced to keep the migration as painless as possible, e.g. the default flex-direction being column. But of course, there are also some breaking changes, and this migration would have to be published in a new major version of the relevant react-pdf packages. Here is a list, compiled by AI and enhanced with my personal estimations of impact on devs using react-pdf:

  • position: static isn't supported by taffy yet. The effect of position: static is that the element ignores its top, left, right and bottom values (this part is emulated in this PR), and that the element isn't considered as a reference frame for nested position: absolute elements (this part is a genuine incompatibility).
    Impact: In react-pdf, position: static is barely mentioned in the react-pdf docs, and AFAIK before last month in the old docs page it wasn't mentioned at all. Support for the feature was introduced in feat: add position: static support #2875 in 2024. So I expect the impact on react-pdf's user base to be rather low.
  • Text in row containers no longer overlaps. Yoga measured text against the container width, then shrank the box but kept the wide lines, so two long Texts in a row drew over each other. Taffy asks for the natural width first and the text is re-broken at its shrunk width. This is a behaviour improvement, but technically a breaking change.
    Impact: I would expect react-pdf users have already worked around yoga's problem, so I estimate low impact.
  • flex: 1 (flex-basis 0) items in auto-height columns get their content height. Yoga collapsed them to 0 and the content overlapped the next sibling. Taffy applies CSS intrinsic sizing. Documents that relied on the collapse will grow.
    Impact: Again, I expect react-pdf users to have worked around this already due to the visible content overlap. Only if someone relies on an element in their code without any content to be collapsed, it could affect them. I'll conservatively say medium impact.
  • Images and SVGs without an explicit size inside a row used to fill row width (Yoga offered the container width). Taffy asks for the intrinsic size, so they now take their pixel size (image) or viewBox size (svg) and shrink to fit the row. In columns they still stretch to the container.
    Impact: This one sounds like it could affect many projects. In my specific project it wasn't a problem, but we don't use a lot of images or SVGs. High impact. Possibly we could emulate the old behaviour (which differs from CSS)..? Or add a feature toggle for legacyYogaCompatibility or similar to activate this emulation?
  • Percentage heights inside auto-height containers resolve to auto (CSS). Yoga resolved them against the parent's available height, so height: 100% on a child of an auto-height column silently inflated the column's flex basis. Documents that relied on this (a layout in my ecamp3 app uses it to stop a sibling flex-grow: 1 header from growing) need an explicit flex-grow: 0 on the sibling instead.
    Impact: This was the one breaking change that I had to adjust in my app. But arguably, it was a bug in ecamp3 that was just masked by yoga's flawed implementation. Therefore, I estimate only medium impact.
  • Text is re-measured at intermediate sizes. Taffy computes the hypothetical cross size of flex-basis: 0 items at a zero (or padding-only) main size before the final one; Yoga never did. This exposed a latent react-pdf bug: relayoutPage re-resolved already resolved styles, multiplying a unitless lineHeight by the font size on every page split. Under Yoga the inflated value was never used because text lines were never recomputed. Fixed by marking resolved nodes (stylesResolved).
    Impact: There are many issues (<Text render={...}> content disappears when an ancestor has lineHeight set #3566 Using lineHeight combined with a "render" function causes text not to render #3083 Rendered Text component content disappears when using inline styles with lineHeight #3044 lineHeight on Page silently breaks render prop on Text elements #3402) about lineHeight leading to text being hidden, which is resolved in this PR. I assume these issue reporters have already worked around the problem, but the frequency of these issues leads me to think more people might be affected. Breaking fix. Impact medium.
  • Things that weren't working with yoga and will work with taffy: Percentage border widths and alignItems: space-between|space-around|space-evenly
    Impact: Low, because these weren't working in yoga or even threw errors, so I expect almost nobody to rely on them.

Bundle size and performance

Due to this comment, I had Claude assess the bundle size, and unfortunately, taffy seems to increase bundle size overall (which is natural, given its bigger feature set over yoga). However, the WASM code is loaded and cached as a separate file, and the javascript-only parts of the bundle therefore actually shrink. TBH, I don't really see the issue with bundle size increasing with increased feature set. I don't imagine rendering a PDF is the first thing an app wants to do on page load. So users with bundle size and first-paint restrictions will probably already lazy-load react-pdf anyways, largely negating the impact of bundle size. Let me know why you think it's still very important @diegomura.

I haven't run any benchmarks yet. In 2023, taffy has run benchmarks against yoga (albeit not through the WASM bindings), and came out on top back then. I don't know what the status is today. Taffy describes itself as "a flexible, high-performance, cross-platform UI layout library written in Rust", so performance issues could at least be raised with them I think.

Annex: Planning and analysis document by claude

TAFFY-MIGRATION.md

carlobeltrame and others added 11 commits September 20, 2026 23:31
taffy-layout wraps DioxusLabs' Taffy (flexbox/grid in Rust) as WASM. The
loader compiles the module once per process, mirroring what the yoga loader
did.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One pure function replaces the 30 yoga setter files. Defaults are pinned to
what Yoga (without web defaults) applied so existing documents keep their
layout: column direction, align-content flex-start, and a 0 automatic
minimum size instead of Taffy's content-based one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- resolveDimensions builds one TaffyTree per page, stores the node id on
  `taffyNode`, computes with rounding disabled and reads the Layout back
  into `box`.
- Measure functions keep their (width, widthMode, height, heightMode)
  signature through a small adapter from Taffy's known/available sizes.
  `max-content` maps to an unconstrained width, which Yoga never asked
  for but Taffy does when sizing flex items in rows.
- measureText caches lines on the node like before, but since Taffy calls
  measure several times per node it re-breaks only unconstrained lines that
  overflow, and lays out with a 0.01pt tolerance because Taffy hands widths
  back as f32. `alignOffset` is derived after layout from the width the
  lines were broken against (tracked in layoutText), instead of during
  measurement.
- The engine instance is no longer threaded through pagination; the
  `resolveYoga` step becomes `resolveTaffy` and only loads the module.
- getMargin/getPadding lose their engine lookups; they are only used inside
  measure functions, where the tree is mid-compute.
- Yoga-specific unit tests for the setters/getters are replaced by
  toTaffyStyle and measureText tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Drops the dependency, the hand-written yoga typings that shadowed it, and
the ignore entries for a vendored yoga package that no longer exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Taffy measures flex items in rows with an unconstrained width. Yoga never
did (it offered the container width), so measureImage and measureSvg had no
answer and images without an explicit size collapsed inside row containers.
Images now report their pixel size, svgs their viewBox size; flex-shrink
still fits them into the row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fills in the differences, breakage and missing-features section of the
migration notes, adds the changeset, and updates the current docs that
named Yoga as the layout engine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Taffy sizes flex items at intermediate main sizes before their final one
(the hypothetical cross size of a flex-basis: 0 item is computed at a zero
main size). Lines broken at such a width must not be kept once the box is
wider, otherwise titles in row headers come out truncated to their first
word. Lines are now reused only when broken at the same width, or at a
wider one while still fitting (with 1pt of tolerance for f32 shrink).

The paginate fuzz test now passes its font store to the paginator, since
relayouts can legitimately re-break text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
splitText slices a node's lines but keeps all of its text children, so
re-laying out a fragment resurrects the whole text and pagination never
converges. Fragments (wasSplit) now always keep their lines.

Taffy measures flex-basis: 0 items at a zero main size before their final
one; breaking text word by word there is pure cost, so zero-width measures
return an empty size without touching the cached lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
relayoutPage runs resolvePageStyles over pages whose styles are already
resolved (only freshly rendered dynamic nodes are new). Resolution is not
idempotent: a unitless lineHeight is multiplied by the font size on every
pass, so after a few page splits a line is thousands of points tall.

Under Yoga this stayed invisible because text lines were never recomputed
after the first layout. With Taffy, text is re-broken when its box grows,
which picked up the inflated lineHeight and made pagination run forever.
Nodes now carry a stylesResolved flag and are skipped on later passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rmediate size

Taffy measures a flex-basis: 0 text at its padding-only width with the
previously measured height as known height. Re-breaking there truncates
every line away, and an empty lines array was then trusted as final, so
the cell rendered empty. Empty lines now trigger a fresh layout (split
fragments excepted), and measurement heights get the same f32 tolerance
as widths so a rounding error cannot drop the last line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PDFs cannot scroll, the stylesheet types only allow overflow: hidden, and
the renderer only clips on hidden. Mapping scroll to Taffy was dead code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
react-pdf Ignored Ignored Preview Sep 23, 2026 10:24am UTC

@changeset-bot

changeset-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 990fa26

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@react-pdf/layout Minor
@react-pdf/renderer Patch
@react-pdf/examples Patch
@react-pdf/math Patch
@react-pdf/mermaid Patch
@react-pdf/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment