Skip to content

[No QA] Serve the web build's Brotli 11 twins from the Cloudflare Worker - #104493

Merged
roryabraham merged 8 commits into
mainfrom
jpersaud-worker-serve-brotli
Oct 22, 2026
Merged

roryabraham merged 8 commits into
mainfrom
jpersaud-worker-serve-brotli

Conversation

@justinpersaud

@justinpersaud justinpersaud commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

  • Serves the smaller files the build already makes: every web build writes a Brotli-compressed copy (foo.js.br) of each JS, CSS, SVG, WASM and font file, but nothing served them. The Worker now sends that copy to browsers instead of letting Cloudflare compress the file on each request, so main downloads 1.12 MB instead of 1.44 MB, and its first byte arrives in 83 ms instead of 210 ms.
  • Keeps browser caching working: a compressed copy has the same ETag as its original, so the Worker gives it its own ("<etag>-br") and answers revalidation (304) itself.
  • Everything else behaves as before: the copies can't be requested at their own URLs (/foo.js.br is a 404), browsers without Brotli get the file decoded by Cloudflare, and files from previous builds are served from the R2 bucket, compressed copy first.

Fixed Issues

$ #101250
For https://github.com/Expensify/Expensify/issues/688126
PROPOSAL:

Tests

  1. In web/cloudflare-worker, run npm test and npm run typecheck and verify all 33 tests and the typecheck pass.
  2. Put a staging build in dist/ and deploy to test-staging: npx wrangler deploy --env test-staging.
  3. Run curl -s -D - -o /tmp/main.br -H 'Accept-Encoding: gzip, deflate, br, zstd' https://test-staging.new.expensify.com/<main bundle> and verify content-encoding: br, an ETag ending in -br", and that /tmp/main.br is byte-for-byte the build's <main bundle>.br.
  4. Repeat with Accept-Encoding: gzip and identity, and verify gzip and the exact original respectively.
  5. For a bundle, service-worker.js, canvaskit-0.41.0.wasm, version.json and favicon.png, send back the received ETag in If-None-Match with each of those Accept-Encoding values, and verify every request gets 304 with no body.
  6. Verify https://test-staging.new.expensify.com/<main bundle>.br, .%62r and .BR return 404, that a navigation to /settings/profile/contact-methods/user@example.com.br gets the app shell, and that the app loads with no console errors.

This is live on test-staging.new.expensify.com right now, though it might be overwritten if we do another deploy.

  • Verify that no errors appear in the JS console

Offline tests

  1. Load https://test-staging.new.expensify.com once so the service worker installs.
  2. Go offline and reload; verify the app opens from the service worker cache (its precache requests the original URLs, which now arrive Brotli-encoded and are decoded by the browser).

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

No QA. Only changes the encoding of the same files on staging and production, verified on test-staging.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified any copy / text that was added to the app is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

The build already writes a Brotli 11 twin of every js, css, svg, wasm and ttf file, about 20% smaller than Cloudflare's edge compression. The Worker now serves the twin with encodeBody: manual, for current assets and previous builds in R2, and Cloudflare decodes it for clients without Brotli. The asset store gives a twin its original's ETag and the edge weakens ETags it re-encodes, so the Worker drops conditional headers from asset requests, gives twins their own ETag and answers If-None-Match itself; otherwise fixed-name files like service-worker.js stopped revalidating with 304.
The Worker serves a previous build's twin from R2 when the current build no longer has the file.
…-brotli

# Conflicts:
#	web/cloudflare-worker/README.md
#	web/cloudflare-worker/src/index.ts
The pre-commit hook formatted them with a newer oxfmt than the repository pins.
The twin check ran on the raw pathname, so /foo.js.%62r reached the asset router, which decodes it and served the raw twin. It also sent any navigation ending in .br, such as an email address on a .com.br domain, to a 404 instead of the app shell. It now checks the decoded path against the twinned file types only.
Falling back to an empty tag gave every such twin the same ETag, so a 304 for one file could answer a revalidation of another.
@justinpersaud
justinpersaud marked this pull request as ready for review October 22, 2026 21:54
@melvin-bot
melvin-bot Bot requested review from eh2077 and removed request for a team October 22, 2026 21:54
@melvin-bot

melvin-bot Bot commented Oct 22, 2026

Copy link
Copy Markdown

@eh2077 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T22:05:04.703266Z e57f1a6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@justinpersaud
justinpersaud requested review from collectioneur and removed request for eh2077 October 22, 2026 21:54

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e57f1a60c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ttf: 'font/ttf',
};
const PRECOMPRESSED_PATH = /\.(?:js|css|svg|wasm|ttf)$/;
const TWIN_PATH = /\.(?:js|css|svg|wasm|ttf)\.br$/i;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block HTML Brotli twins at their own URLs

Production builds also emit HTML twins because config/rsbuild/rsbuild.common.ts:619 includes html, and .assetsignore now uploads every .br file. Since this guard only recognizes JS/CSS/SVG/WASM/TTF twins, a direct request such as /index.html.br bypasses the early 404 and is served by Static Assets, contrary to the documented invariant that twins are never available at their own URLs. Include HTML in the direct-twin guard while continuing to exclude it from PRECOMPRESSED_PATH so nonce-bearing HTML is not selected as an encoded response.

Useful? React with 👍 / 👎.

@roryabraham roryabraham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@roryabraham

Copy link
Copy Markdown
Contributor

Going to merge this and CP to staging to test.

@roryabraham
roryabraham merged commit 8f6d468 into main Oct 22, 2026
47 of 50 checks passed
@roryabraham
roryabraham deleted the jpersaud-worker-serve-brotli branch October 22, 2026 23:10
@github-actions

Copy link
Copy Markdown
Contributor

🚧 roryabraham has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify OSBotify added the CP Staging marks PRs that have been CP'd to staging label Oct 22, 2026
@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Cherry-picked to staging by https://github.com/roryabraham in version: 9.5.20-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@roryabraham

Copy link
Copy Markdown
Contributor

@MelvinBot now that this is live on staging, check if it's working as expected

@MelvinBot

Copy link
Copy Markdown
Contributor

The Worker works on staging (9.5.20-0), but Chrome, Edge and Firefox users don't get the Brotli copy yet. Those browsers send zstd in Accept-Encoding. On staging, Cloudflare then decodes the Brotli copy and re-encodes it as zstd: 1.46 MB instead of 1.13 MB. That's about the same as production today (1.45 MB zstd). test-staging sends br for the same request, so the staging and production hostnames probably have a Cloudflare compression setting that prefers zstd. That setting lives in Cloudflare, not in this PR.

main-e27519ace5234214.bundle.js on staging, by Accept-Encoding:

Accept-Encoding Encoding Size ETag
gzip, deflate, br, zstd (Chrome) zstd 1.46 MB W/"…-br"
gzip, deflate, br (Safari) br 1.13 MB "…-br"
gzip gzip 1.60 MB W/"…-br"
identity none 5.66 MB W/"…-br"
Other checks (all pass)
  • Decoding the br body gives the same 5.66 MB file.
  • The CSS and service-worker.js are also sent as br with a -br ETag. version.json is unchanged.
  • Sending the strong or weak -br ETag in If-None-Match returns 304 with no body, with or without zstd and with identity.
  • /main-….bundle.js.br, .%62r and .BR return 404.
  • Navigating to /settings/profile/contact-methods/user@example.com.br returns the app shell (200 text/html).
  • An older build's bundle (main-5fb77d2779c08ca8) loads from R2 with Cloudflare's own br (1.41 MB, no -br ETag). That's expected: earlier deploys didn't upload the .br copies.
  • Time to first byte, one sample each from this runner: 122 ms for zstd and 195 ms for br. That's too few samples to judge speed.

I tested with curl. I didn't load the app in a browser.


view run

@roryabraham

Copy link
Copy Markdown
Contributor

Tried to the find relevant compression rules in Cloudflare, I'm not sure I have access to them.

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

Labels

CP Staging marks PRs that have been CP'd to staging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants