Skip to content

Lazy-load Defuddle and the reader's Vue app in inject-css - #910

Draft
ankit wants to merge 1 commit into
worktree-on-demand-editor-injectionfrom
worktree-lazy-readability-import
Draft

Lazy-load Defuddle and the reader's Vue app in inject-css#910
ankit wants to merge 1 commit into
worktree-on-demand-editor-injectionfrom
worktree-lazy-readability-import

Conversation

@ankit

@ankit ankit commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Stacked on #908 — this targets that branch, not main.

Summary

Following up on the perf discussion in #908: inject-css's own eager bundle was still surprisingly large (~592KB raw / 166KB gzip) even after moving editor/index.js off the eager path. Investigated with webpack's module stats and found why — apply-state.ts imported applyReadability/removeReadability from the full @stylebot/readability barrel, which unconditionally pulls in:

  • defuddle (the Readability parsing engine): 335 KB
  • the full vue runtime (needed only for the reader's own tiny App.vue): 227 KB
  • the barrel's re-export/SCSS glue: 110 KB

That's ~460KB of minified JS shipped and evaluated on every page load for every user, even though only a minority ever have Reader Mode enabled on any site.

Change

  • applyReadability is now behind a dynamic import('@stylebot/readability'), code-splitting Defuddle + the reader's Vue app into their own chunk(s), fetched only on pages where a saved style actually has readability: true.
  • removeReadability (the far more common "readability off" path) doesn't touch Defuddle/Vue at all, so it now imports directly from ../readability/lifecycle/remove-readability, bypassing the barrel entirely — same technique inject-css/index.ts already used for isReaderable.
  • Content scripts run in the page's origin, so webpack's default (page-relative) chunk-loading URL would try to fetch the dynamic chunk from the site being styled instead of the extension. Fixed by overriding __webpack_public_path__ to chrome.runtime.getURL('/') for this entry, guarded so it's a no-op under ts-jest (no webpack runtime).
  • The two resulting chunk files (webpack's default vendor-splitting produces readability-lazy.chunk.js + vendors~readability-lazy.chunk.js) are covered by a new *.chunk.js entry in web_accessible_resources.
  • Not Chrome/Edge-specific — unlike On-demand inject the editor UI on Chrome/Edge #908, this touches shared inject-css code used identically by Firefox, so Firefox benefits too. Verified the Firefox build also produces and correctly references the chunks.

The one deliberately-preserved behavior: applyReadability()'s call itself stays fire-and-forget from apply-state.ts (not part of its returned/awaited promise) — same as before — since that promise's resolution timing drives revealPage(), and coupling it to Reader Mode's own async mount (which waits on DOMContentLoaded/load) would have been a much bigger, riskier behavior change than intended here.

Perf impact

Measured on this branch vs. its base (#908), same production webpack config:

inject-css/index.js (eager)
base (#908) 594 KB raw / 166 KB gzip
this branch 151 KB raw / 44 KB gzip

A further ~75% reduction in the eager bundle for any page that doesn't have Reader Mode enabled — on top of #908's already-large win from making editor/index.js on-demand.

Test plan

  • yarn typecheck, yarn lint
  • yarn test — updated apply-state.test.ts for the new import paths, all 238 tests pass
  • yarn test:e2e — new lazy-readability-import.spec.ts asserts (a) the chunk is never fetched when readability is off, and (b) it loads successfully (200 responses) when on — directly exercising the dynamic import + publicPath override + web_accessible_resources wiring in a real browser, independent of Defuddle's own content-extraction heuristics
  • Verified BROWSER=firefox build also produces the chunks and inherits the manifest entry correctly

Note: the full e2e suite is reliable run serially (--workers=1, confirmed twice); under this sandbox's default parallelism a couple of tests intermittently time out from what looks like multiple simultaneous Chrome instances contending for resources on this machine, not a functional issue — worth a sanity check in CI.

🤖 Generated with Claude Code

apply-state.ts imported applyReadability/removeReadability from the
full @stylebot/readability barrel, which unconditionally pulled
Defuddle and the reader's own Vue app into the always-eager inject-css
bundle — paid on every page load regardless of whether that page ever
uses Reader Mode. applyReadability is now behind a dynamic import(),
code-splitting that ~460KB out into its own chunk fetched only on
pages where a saved style actually has readability enabled.
removeReadability (used on the far more common "readability off" path)
doesn't need Defuddle/Vue at all, so it now bypasses the barrel
directly instead, same as the existing isReaderable import.

Requires overriding __webpack_public_path__ for this content script,
since its document is the target page's own origin, not the
extension's — otherwise webpack's default chunk-loading tries to fetch
the dynamic chunk from the page being styled instead of the extension.
The two resulting chunk files are covered by a new *.chunk.js entry in
web_accessible_resources.

Unlike the on-demand editor-injection change this stacks on, this one
isn't Chrome/Edge-specific — Firefox shares the same inject-css code
and benefits identically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant