Skip to content

Speed up frontend builds: patch pofile, switch to SWC - #1415

Merged
daveearley merged 1 commit into
developfrom
fix/frontend-build-speed
Oct 18, 2026
Merged

daveearley merged 1 commit into
developfrom
fix/frontend-build-speed

Conversation

@daveearley

@daveearley daveearley commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Why

Staging frontend deploys on DigitalOcean went from ~6.5 min to ~25 min starting with #1411 (6m 54s on Oct 17 06:21 → 17m 46s at 21:37 → 24m 54s today).

Phase timings from the DO build logs:

Step Before (122ef58) After (b30bc05)
lingui extract 13s 42s
lingui compile 23s 5m 7s
client vite build 1m 9s 6m 36s
SSR vite build 39s 5m 33s

The code barely changed (+37 modules), but each locale .po file grew from 15,926 to 16,669 lines. That crossed V8's ~16.4k-element limit for constant-time Array.prototype.shift(). pofile (used by @lingui/format-po) parses with while (lines.length > 0) { lines.shift() … }, so each parse turned quadratic: about 9ms → 55ms per catalog. Lingui parses every catalog many times per locale, in lingui compile and again in the Vite plugin for both the client and SSR builds, so every step slowed 5–10x. pofile has had no release since 2022.

What

  • Patch pofile with patch-package to iterate by index. Parse time is flat again (~150ms for all 21 catalogs at every commit tested).
  • Babel → SWC: @vitejs/plugin-react-swc + @lingui/swc-plugin@4.1.0 replace @vitejs/plugin-react with the Babel macros plugin. @swc/core is pinned to 1.7.42 via resolutions, the range the Lingui 4 SWC plugin supports.
  • Drop lingui compile from build and Dockerfile.ssr. The app loads catalogs with import(\./locales/${locale}.po`)through@lingui/vite-plugin, so the compiled .jsoutput is never used.messages:extractstays inbuild`: it's the only guard against a string missing from the catalogs, which would render as its hashed id in production.
  • Docker: copy patches/ into the install stages so the patch applies. The production-only install in Dockerfile.ssr drops the postinstall hook, because neither patch-package nor pofile is installed there.

Results (CI, Dockerfile.ssr build step on GitHub runners)

lingui compile client build SSR build total
develop before the slowdown (122ef58) 14s 33s 24s ~72s
develop now (b30bc05) 77s 2m 6s 1m 37s ~302s
this PR (removed) 1m 6s 35s ~104s

The client build is still above the pre-slowdown figure because #1412 added the Scalar API reference (+5,900 modules). On this PR's first run, the step's total wall time also includes an uncached dependency install (the lockfile changed) and re-exporting every layer to the Actions cache.

Results (local, Node 24, Apple Silicon)

Step Before After
lingui compile 17.4s 8.0s (and no longer in the build)
client vite build 30.6s 19.4s
SSR vite build 22.5s 11.0s

DO's builder is roughly 5–10x slower than a laptop on these CPU-bound steps.

Behaviour checks

  • Message ids in the client bundle match the Babel build, with two extra ids. Those are nested t macros (t\Sold to ${name ?? t`an attendee`}`) that Babel never expanded: the inner tshipped as a call to whatever minified variable was namedt`. SWC expands them correctly, so this also fixes that.
  • Production stripping still applies (same asset size, no inline source strings).
  • The production SSR server renders translated pages (de, fr) and hydrates. The Dockerfile.ssr image builds, applies the patch in deps, and serves pages.
  • In the dev server, macros are transformed and Fast Refresh is wired.
  • lingui extract leaves the catalogs unchanged, and lingui compile output is byte-identical.

Notes

  • The dev stack needs a rebuild after pulling, because the frontend container keeps its own node_modules volume: docker compose -f docker-compose.dev.yml up -d --build -V frontend

Frontend builds on DigitalOcean went from ~6.5 to ~25 minutes once the .po
catalogs passed ~16.4k lines. pofile parses with `while (lines.shift())`,
and V8's Array.shift becomes O(n) for arrays that size, so every catalog
parse turned quadratic. lingui compile, the client build and the SSR build
all parse every catalog repeatedly, so all three slowed down 5-10x.

- Patch pofile (patch-package) to iterate by index
- Compile with @vitejs/plugin-react-swc + @lingui/swc-plugin instead of
  Babel macros; @swc/core pinned to 1.7.42, the range the Lingui 4 plugin
  supports
- Drop `lingui compile` from the build: the app imports the .po catalogs
  through the Lingui Vite plugin, so the compiled .js output is unused
- Copy patches/ into Docker install stages; skip postinstall in the
  production-only install, which has neither patch-package nor pofile
@railway-app

railway-app Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the Hi.Events-pr-1415 environment in Hi.Events

Service Status Web Updated
Hi.Events Frontend 😴 Sleeping (View Logs) Web Oct 18, 2026 at 12:43 pm UTC
Hi.Events API ✅ Success (View Logs) Web Oct 18, 2026 at 12:21 pm UTC
3 services not affected by this PR
  • Hi.Events Scheduler
  • Hi.Events Queue Worker
  • Maildev

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ E2E full suite (pull request): Everything passed, but 4 tests only passed on retry.

Mode Result ✅ Passed ❌ Failed ⚠️ Flaky ⏭️ Skipped ⏱️ Duration
Self-hosted ⚠️ Passed (some flaky) 592 0 2 4 14m 28s
SaaS ⚠️ Passed (some flaky) 580 0 2 16 13m 49s

⚠️ Flaky tests: failed first, then passed on retry (4)

  • SaaS · seating/seating-flag-off.spec.ts:92 · reserved seating with the feature flag off on Hi.Events Cloud › a seated event cannot be duplicated into unseated tickets while seating is off
  • SaaS · widget/widget-editor.spec.ts:104 · widget editor › organizers embed their events list from the organizer area
  • Self-hosted · widget/widget-editor.spec.ts:104 · widget editor › organizers embed their events list from the organizer area
  • Self-hosted · widget/widget-playground.spec.ts:29 · widget playground › the playground embeds an organizer events list as a custom element
⏭️ Why tests were skipped

Self-hosted

  • 3 × Runs only against a SaaS stack (E2E_SAAS_MODE=true).
  • 1 × Lifecycle emails only exist in SaaS mode.

SaaS

  • 15 × Runs only against a non-SaaS stack (E2E_SAAS_MODE=false).
  • 1 × The composer form is gated behind Stripe/manual account verification in SaaS mode.

Open the run

@daveearley
daveearley merged commit 0e3467a into develop Oct 18, 2026
12 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 18, 2026
@daveearley
daveearley deleted the fix/frontend-build-speed branch October 18, 2026 16:23

This branch was successfully deployed

No deployments
Hi.Events / Hi.Events-pr-1415 — 55bd2c7b Deployed Oct 18, 2026 by railway-app[bot]
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant