SYSTEM Cited by 3 sources
Zalando Rendering Engine¶
What it is¶
The Rendering Engine is the runtime core of Zalando's Interface Framework. It is a Node.js backend service and a browser runtime that, for any given request, resolves a tree of Entities and transforms it into a tree of matching Renderers to produce the page the customer sees (Source: sources/2021-03-10-zalando-micro-frontends-from-fragments-to-renderers-part-1).
It sits at the centre of the IF architecture:
GraphQL API ──→ Rendering Engine ──→ HTML + hydrated React
(Node.js server;
browser runtime)
uses:
- rendering rules (Entity → Renderer)
- Zalando Design System
- Renderers contributed by feature teams
What it does¶
Per the Part 1 post (Source: sources/2021-03-10-zalando-micro-frontends-from-fragments-to-renderers-part-1):
- Receives an Entity tree whose structure is chosen by Zalando's Recommendation System (or other personalisation services) for this specific request and customer. An example from the post: a product page's in-article slots get filled with "a collection, an outfit, and another collection" chosen server-side.
- Applies rendering rules — a declarative mapping from Entities to Renderers — to pick how each Entity in the tree should be visualised. The same Entity type can map to several Renderers depending on placement (an Outfit Entity might render as a main view, or as a card inside a Collection).
- Invokes each Renderer, a self-contained piece of React that declares its GraphQL data dependencies and pulls from the GraphQL aggregation layer.
- Renders via the Zalando Design System, which means the Rendering Engine takes over component version management and client bundle-size optimisation across Renderers — feature teams do not hand-tune either.
Hybrid rendering modes¶
A single Rendering Engine can serve three kinds of view configuration (Source: sources/2021-03-10-zalando-micro-frontends-from-fragments-to-renderers-part-1):
- Mosaic Template only — legacy path; view is composed entirely of Mosaic Fragments.
- Mixed — view contains Renderers and Fragments.
- Renderers only — the modern IF path.
This hybrid capability is the migration load-bearer: it let Zalando swap Fragments for Renderers incrementally while keeping a single serving substrate. By March 2021, ~90% of traffic is served through the Rendering Engine, with the residual hybrid-mode tail mostly Mosaic-era Fragments not yet rewritten.
Why it runs in both Node.js and the browser¶
The post names the Rendering Engine explicitly as running "in Node.js and the browser" (Source: sources/2021-03-10-zalando-micro-frontends-from-fragments-to-renderers-part-1). That is a universal-rendering shape (see concepts/universal-rendering): the same tree-resolution and Renderer-invocation logic executes server-side to produce the initial HTML response, and client-side to resume rendering, hydrate, and re-render as the Entity tree or personalisation state changes. Keeping one Engine on both sides is what makes the "Renderer" a single unit that contributors write once and trust to run in both environments.
Known gaps (Part 1 post)¶
The Part 1 post names the Rendering Engine but does not describe:
- the rendering rules language / DSL beyond "declarative set of layout rules",
- whether it supports streaming SSR (concepts/streaming-ssr) or classic SSR,
- the caching strategy (per-Renderer output cache? per Entity subtree?),
- failure isolation across Renderers — if one Renderer throws, what does the page do?
- the concurrency / backpressure model of the Node.js server,
- the exact hot path for a page with 10+ Renderers.
These are flagged for upcoming posts in the series.
React 18 / Concurrent rendering migration (2023)¶
The 2023-07 Rendering Engine Tales post extends the RE story with a React 18 migration chapter, answering one of the gaps above ("streaming SSR? classic SSR?") retroactively: pre-React-18 RE already had partial hydration, partial streaming, and lazy-loading built in-house, and the React 18 migration is partly about delegating those to a supported upstream implementation.
Architectural fit¶
"Rendering Engine's own partial hydration/streaming features! … React's concurrent rendering APIs seamlessly integrate with the architecture of RE because its Renderers serve as ideal candidates for being encapsulated within a Suspense boundary. This enables them to function as individual blocks that can be server-rendered, streamed, hydrated, and client-rendered 'concurrently'." (Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react.)
Each Renderer → one <Suspense> boundary is the migration's
core architectural mapping. The Renderer-per-Entity granularity
(see patterns/entity-to-renderer-mapping) turns out to be
the right grain for React 18's per-boundary
progressive hydration.
Design choice: Application-State layer outside React¶
Rather than move data resolution into React hooks (the Render-As-You-Fetch pattern), RE created an Application-State layer that sits outside React and dictates Suspense state from the outside. See patterns/application-state-layer-outside-react for the canonical pattern page.
Four upstream blockers drove this decision away from pure hook-based RAYF:
SuspenseListis experimental with limitations (breaks RE's ordered-streaming requirement).useTransitiondoesn't consider nested Suspense boundaries (bad UX in RE's deeply-nested Renderer trees).- Hook-initiated fetches couple fetch timing to render order (performance anti-pattern when siblings could parallelise).
- React's streaming/caching layer for data supply (facebook/react#25502) wasn't final at PoC time.
Pipeline shape:
resolveEntity (RE) → writes to → Application State
│
│ read
▼
Connector hook
(returns data OR throws Promise)
│
▼
<Suspense>-wrapped Renderer
The Connector hook is framed in the post as "Imagine Redux's
useSelector hook, but instead of immediately returning
selected data you get a Promise that only resolves once a
reducer has made the data available."
Measured impact of the narrow API swap¶
A milestone before the full Application-State-layer rollout:
RE's internal streaming + hydration APIs were swapped to React
18 equivalents — renderToPipeableStream replaced
renderToNodeStream; hydrateRoot replaced hydrate. No
other concurrent-feature adoption. Rolled as an A/B test
across all Fashion Store pages
(Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react):
Overall:
| Metric | Δ |
|---|---|
| INP | −5.69 % |
| FID | −8.81 % |
| LCP | −2.43 % |
| FCP | −0.23 % |
| Bounce rate | −0.24 % |
Per page:
| Metric | Home | Catalog | PDP |
|---|---|---|---|
| INP | −2.92 % | −6.76 % | −6.09 % |
| FID | −2.98 % | −17.11 % | −6.06 % |
| Exit Rate | −0.43 % | −0.06 % | −0.06 % |
Interactive-latency metrics improved most; the biggest wins were on Catalog (the most Renderer-dense page).
Hydration-mismatch taxonomy surfaced¶
The React 18 hydration contract is much stricter than React 17's, and the migration surfaced dozens of different types of issues across hundreds of Renderers. The four load-bearing categories documented in the post (see concepts/hydration-mismatch for the full taxonomy):
- Timers / time-deltas — SSR and CSR compute the delta at
different instants. Fix:
suppressHydrationWarning={true}(patterns/suppress-hydration-warning-for-unavoidable-mismatch). - Timezone-localised dates —
Intl.DateTimeFormatdefaults to host timezone; Node-in-UTC and the user's browser disagree. Fix: pass explicittimeZone, or move localization to backend (patterns/backend-localization-for-hydration-stability). - Number locale / Safari de-AT
Intl.NumberFormatbug — Safari's thousand-separator forde-ATis"."while Node's is narrow-no-break-space. Unfixable application-side (it's below the app layer). Fix: backend-localise. - Invalid HTML nesting (
<div>in<p>,<button>in<button>) — React 18 treats structurally-invalid HTML as a mismatch. Fix: semantically valid nesting, enforced viaeslint-plugin-validate-jsx-nesting.
General escape hatch for genuinely-client-only content: patterns/mount-gated-client-only-rendering.
Sentry integration and error dedup¶
React's onRecoverableError fires multiple times per single
mismatch because post-first-error the fiber-vs-DOM list
alignment breaks and subsequent nodes also fail. Zalando's
production fix is to only forward the first hydration error
per session to Sentry — see
patterns/first-error-only-hydration-error-reporting.
Deferred to future posts¶
- Ordered streaming/hydration technical solution: "We will share the details of the technical solution for ordered streaming/hydration in another post."
- Final architecture effects on Fashion Store performance (beyond the narrow-API-swap A/B-test deltas above).
- React Server Components adoption.
Extension to mobile (2025-10)¶
As of Zalando's 2025-10-02 disclosure (sources/2025-10-02-zalando-accelerating-mobile-app-development-with-rendering-engine-and-react-native), Rendering Engine is no longer web-only. Zalando is using Rendering Engine as the application-layer foundation for their React Native migration of the mobile app (previously two separate native codebases, 90+ screens).
Key adaptation:
- Same Renderer abstraction — the "supercharged React components with observability, metrics, traces, data fetching, caching, state management, and analytics built-in" primitive is reused on RN. Zalando reports that integrating the web-era framework into RN yielded a production-ready setup with live data access in "just a few weeks."
- Cross-platform UI substrate — paired with react-strict-dom + StyleX for HTML-subset cross-platform UI (see patterns/html-subset-to-native-ui-mapping) and Metro file resolution for per-platform escape hatches.
- Brownfield integration via RN-as-a-package — Rendering Engine + RN run inside a Framework SDK that the legacy native app links as any other framework.
- Validated screen — Discovery Feed, the media-heavy front screen, is one of the migrated RN surfaces.
This extension validates a multi-year architectural bet: the Renderer abstraction designed in 2018 for web-era micro- frontends turned out to be portable to mobile without being restructured. "Reusing our Rendering Engine allowed us to share core functionalities and code across both app and web platforms" — canonical end-state of the Interface-Framework unification posture.
Seen in¶
- sources/2021-03-10-zalando-micro-frontends-from-fragments-to-renderers-part-1 — origin story: Rendering Engine as the runtime core of the second-generation Interface Framework; Entity tree → Renderer tree transformation.
- — the Renderer abstraction's internals, contributed-by-teams model, and observability story.
- sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react — the React 18 concurrent-rendering migration; streaming SSR, hydration-mismatch taxonomy, Application-State-layer pattern.
- sources/2025-10-02-zalando-accelerating-mobile-app-development-with-rendering-engine-and-react-native — the mobile extension. Rendering Engine powers Zalando's React Native migration; Renderer abstraction extends cross-platform (web + iOS + Android); paired with react-strict-dom + StyleX + Metro as the cross-platform UI stack.
- — the editorial-content extension. Rendering Engine serves Zalando's Landing Pages stack: modules authored in Contentful come through FSA via the Contentful proxy and each module maps to a Renderer — same Entity-to-Renderer substrate, CMS-driven module list instead of personalisation- driven entity tree.
- — the context-based-experience extension. The Rendering Engine's request state gains a resolved- Experience slot, populated during root-entity resolution (concepts/root-entity-experience-resolution) from the owning domain backend's selection-rule match against selection metadata. Every subsequent child-renderer GraphQL query the RE issues to FSA carries the resolved Experience name, so downstream backends apply the right presentation policies without rederiving intent — canonical wiki instance of request-state- propagated presentation context. The Rendering Engine does not itself classify customer intent: that lives in Catalog / Search / Product backends. The RE is the request-state courier plus the child-fan-out orchestrator.
Related¶
- systems/zalando-interface-framework · systems/zalando-mosaic · systems/zalando-graphql-ubff · systems/nodejs · systems/react · systems/react-native · systems/react-strict-dom · systems/stylex · systems/zalando-mobile-framework-sdk · systems/zalando-mobile-developer-app · systems/zalando-discovery-feed · systems/sentry
- concepts/entity-based-page-composition · concepts/micro-frontends · concepts/universal-rendering · concepts/streaming-ssr · concepts/concurrent-rendering-react · concepts/react-hydration · concepts/hydration-mismatch · concepts/progressive-hydration · concepts/render-as-you-fetch · concepts/react-native-as-a-package · concepts/css-subset-cross-platform-ui · concepts/progressive-screen-level-rn-migration
- patterns/entity-to-renderer-mapping · patterns/application-state-layer-outside-react · patterns/suspense-boundary · patterns/suppress-hydration-warning-for-unavoidable-mismatch · patterns/mount-gated-client-only-rendering · patterns/backend-localization-for-hydration-stability · patterns/first-error-only-hydration-error-reporting · patterns/rn-as-consumable-npm-entry-point · patterns/screen-by-screen-rn-migration · patterns/html-subset-to-native-ui-mapping
- companies/zalando
As the backend for Appcraft¶
The 2024-05-15 Appcraft retrospective () explicitly references the 2021-09 micro-frontends part-2 post (= this Rendering Engine's architecture post) as "the backend system that empowers this platform".
The connection is the custom React reconciler path documented in part 2 — for Native Apps, the same Renderer React element tree is consumed by a custom reconciler that emits app-compatible JSON instead of HTML. That JSON is precisely the wire format that Appcraft consumes on iOS + Android, where it is rendered via Flex on top of Texture / Litho into native UIKit / RecyclerView objects.
So the Rendering Engine is bidirectional:
- Web surface: standard React renderer → HTML (streamed SSR) → browser hydration.
- Mobile surface: custom React reconciler → JSON → Appcraft client runtime → Texture / Litho → native UI.
This places Appcraft as the mobile-native consumer of the JSON wire format the Rendering Engine produces; both share the same Renderer authoring model on the server side.