Skip to content

FastBoot 7: render with a DOM, for classic and Vite apps - #990

Draft
NullVoxPopuli-ai-agent wants to merge 4 commits into
ember-fastboot:masterfrom
NullVoxPopuli-ai-agent:nvp/fastboot-vite-ssr
Draft

NullVoxPopuli-ai-agent wants to merge 4 commits into
ember-fastboot:masterfrom
NullVoxPopuli-ai-agent:nvp/fastboot-vite-ssr

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Oct 27, 2026 •

Copy link
Copy Markdown

FastBoot 7 renders the app with a DOM, the same way a browser runs it, for classic (broccoli) apps and Vite apps. This is a breaking change. It is the SSR path that emberjs/rfcs#1178 asks for: FastBoot no longer needs isBrowser, isInteractive, hasDOM, or the -document service.

What changes for apps and addons

  • On the server, document, window, location, and fetch exist, and modifiers run. Apps and addons write normal browser code, with no if (fastboot) checks.
  • One build serves the browser and the server. There are no fastboot/ trees, no FastBoot global, and no sandbox.
  • The browser always takes over the rendered page (rehydration). It does not render the page again.
  • The fastboot service stays, for the upgrade. shoebox.put() and fastboot.response now work in the browser too (they do nothing there), so code that uses them needs no condition.
  • The docs tell apps and addons to write browser code. They tell new code not to use isFastBoot.

UPGRADING.md has the full list of removed options.

How it works

  • fastboot runs the app in worker threads. In each worker, @happy-dom/global-registrator makes window === globalThis. Ember's own hasDOM check then sees a browser, so FastBoot passes only rootElement and _renderMode: 'serialize'.
  • A classic dist loads from its index.html, script by script. A Vite build loads from its SSR bundle, which exports createSsrApp().
  • fastboot/vite renders pages in the Vite dev server with ssrLoadModule.
  • SSG: prerender() from fastboot/prerender, and the prerender option of fastboot/vite, write a static page for each route at build time. Each page links the stylesheets of its lazy routes, from the Vite manifest. With prerender, the dev server renders only those routes, into the same template. Spike: prerender the docs with FastBoot instead of vite-ember-ssr NullVoxPopuli/limber#2298 uses it in place of vite-ember-ssr.
  • fastboot/middleware is a (req, res, next) handler for node:http, Express, Connect, and Vite.
  • ember-cli-fastboot is a v2 addon. A small v1 shim gives classic apps ember serve rendering and the boot code.

Decisions to check

  1. I removed fastboot-express-middleware and fastboot-app-server. fastboot/middleware replaces the first. The app server's cluster and its deploy downloaders and notifiers have no replacement. If you want to keep the app server, I can port it to the new API.
  2. patches/@embroider__core@4.6.10.patch fixes a crash in every Vite app with this addon. Embroider walks <app>/_fastboot_ for any active addon named ember-cli-fastboot, and that directory does not exist. The fix is on the branch nvp/fastboot-dir-optional of the agent's Embroider fork, and I have not opened that PR yet. A release of this PR needs that fix released first.
  3. The peer range is ember-source >= 6.4. CI tests only ember-source 7.4.
  4. CI runs on Ubuntu only. The old CI also ran Windows.

Tests

pnpm test builds two fixture apps (a classic app, and the default Vite blueprint with WarpDrive), then runs 83 tests with Vitest:

  • every route on both builds: modifiers, title, redirects, status codes, request data, shoebox, deferRendering, run loop timers, <html> and <body> attributes, errors, concurrent renders, and reload()
  • the request handler, the Vite dev server (with an edit to a source file), and ember serve
  • Chromium: the browser keeps the server's DOM nodes, renders no second copy, and logs no errors
  • prerendering: the pages of a classic and a Vite build, a route that Embroider splits, its stylesheet with JavaScript turned off, and the dev server with prerender

All tests pass locally on Node 22.22, 24.21, and 26.12.

🤖 Generated with Claude Code

NullVoxPopuli and others added 4 commits October 27, 2026 18:16
BREAKING CHANGE: FastBoot renders the app the same way a browser runs it.

- fastboot runs the app in worker threads. happy-dom makes each worker
  a browser window (window === globalThis), so Ember's own environment
  check sees a browser. FastBoot passes no isBrowser, isInteractive, or
  document boot options, and does not use the -document service.
- One loader reads a classic dist from index.html. Another imports the
  SSR bundle of a Vite build (createSsrApp). The fastboot/vite plugin
  renders pages in the Vite dev server with ssrLoadModule.
- fastboot/middleware replaces fastboot-express-middleware.
  fastboot-app-server is removed.
- ember-cli-fastboot is a v2 addon with a v1 shim for classic builds.
  It keeps the fastboot service for the upgrade. boot() rehydrates a
  rendered page in the browser.
- Removed: the vm sandbox, the FastBoot global, fastboot/ trees, the
  dist manifest, the fastboot location, and result.chunks().
- pnpm 12, Node 22.12 or later, CI on Node 22, 24, and 26. Tests use
  vitest, a classic and a Vite fixture app, and Chromium for
  rehydration.

The @embroider/core patch fixes a crash: Embroider walks <app>/_fastboot_
for any active addon named ember-cli-fastboot, and that directory does
not exist in a Vite app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `fastboot/prerender`: `prerender({ distPath, entry, routes, template, outDir })`
  renders each URL and writes `<outDir>/<url>/index.html`.
  It rejects with an AggregateError when a page fails or has a status of 400 or more,
  so a broken page fails the build.
- `fastboot/vite`: the `prerender: { routes, template }` option. After the client
  build, the plugin runs the SSR build with the same config into a temporary
  directory, prerenders the routes, and removes the SSR bundle.

The temporary directory is outside node_modules: an SSR build that writes into
node_modules leaves the app modules of an Embroider app unresolved (`./app`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A route chunk that loads lazily has its own CSS, and the entry of the page
does not link it. Without the link, a prerendered page shows that route
unstyled until the JavaScript loads.

- `Result#routeNames`: the active routes after a render.
- The Vite plugin reads the manifest of the client build, maps source
  modules to route names, and links the CSS of each route of the page.
  The default `routeName` reads the routes that Embroider splits
  (`-embroider-route-entrypoint.js:route=<name>`) and the classic
  `app/templates`, `app/routes`, and `app/controllers` paths.
  `prerender.routeName` sets another layout.
- `prerender()` takes `transformHtml(html, { url, result })`.
- If the plugin turned on the manifest, it removes it after the prerender.

The Vite fixture splits a new `lazy` route with its own CSS. A Chromium test
with JavaScript turned off checks that the prerendered page has its styles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- With `prerender`, the dev server renders only the prerender routes, and
  renders them into `prerender.template`. Before, it rendered every page
  into index.html. An app whose index.html boots without rehydration (the
  template does the rehydration) then showed each page twice.
- `FastBoot` takes a `template` option: the HTML file to render into.
- Vite calls `closeBundle` after a failed build too. The prerender now runs
  only after `writeBundle`, so a build error is no longer hidden behind a
  missing manifest.

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

Copy link
Copy Markdown
Contributor

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

if someone could put this in draft, that'd be great.

I haven't reviewed the code yet, only outputs

@NullVoxPopuli-ai-agent
NullVoxPopuli-ai-agent marked this pull request as draft October 29, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants