Skip to content

perf: cut main.js by 10% with a fuse.js search port and leaner build output - #407

Merged
chhoumann merged 14 commits into
masterfrom
perf/bundle-diet
Oct 12, 2026
Merged

chhoumann merged 14 commits into
masterfrom
perf/bundle-diet

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

main.js was 35 bytes under its 100,000-byte gzip gate, and the next units need room. This PR cuts it from 99,965 to 89,833 gzip bytes (-10.1%) with no change users can see.

  • Replace fuse.js with src/utility/fuzzySearch.ts, a port of the Fuse.js 7.5.0 Bitap search that PodNotes used for episode search and the "Did you mean" tag hint. Results rank exactly as before, and a recorded fuse.js fixture now guards that in CI. The per-list index cache goes too. About -8,600 gzip bytes.
  • Strip CSS comments from the styles Svelte injects into main.js at build time, keeping /*! license notices, so comments stay in source. PostCSS, already in the toolchain through Vite, removes only comments between rules and declarations, so strings, url() and comments that separate tokens stay intact. About -1,030.
  • Drop pure annotations from the final bundle, which nothing bundles again. -479.
  • Drop dead vendor prefixes and commented-out rules from the loading spinner. -166.

Left out on purpose: raising the build target to es2022 would save another 1,340 bytes, but Safari before 16 throws on some native class field initializers (WebKit bug 236843), and Obsidian 1.13 still supports iOS 15. Smaller cuts the audit found (duplicate path helpers, a regex literal, a lookup table) save under 60 bytes each. The gate is unchanged, and nothing moves out of main.js.

Verification

  • npm run size: 317,450 → 282,491 raw, 99,965 → 89,833 gzip against master.
  • Lint, format check, typecheck, build, unit tests (1,586) and docs build pass on Node 22.23.2 after merging master aa6adda. npm run test:e2e passes all 68 tests.
  • In isolated Obsidian 1.13.7 on Linux: the settings, grid, episode list, player and mobile-emulation screens are pixel-identical to master. A scripted pass that adds a feed, searches with typos, plays, captures a timestamp, triggers "Did you mean", creates a note, opens a timestamp link and runs a direct obsidian://podnotes URI gives identical results on both builds, with no runtime errors.
  • Search ranking: the final port matched real fuse.js 7.5.0 in about 122,000 independent reviewer comparisons, and the committed fixture fails for every ranking-changing mutation found. The first version of the port matched in 405,000 comparisons. Search per keystroke on 2,000 titles is faster: 6.37 → 5.02 ms (median of 25 runs per side).
  • Cold plugin load, as Obsidian's startup report records it, shows no significant difference. Four interleaved runs of 22 to 30 fresh starts per side gave medians of 4.05 → 3.55, 5.45 → 6.95, 6.10 → 5.55 and 5.2 → 4.15 ms on a shared host (p = 0.11 to 0.38). Read and compile, the half of the ~3.5 ms load that scales with bundle size, operate on an 11% smaller file.

Evidence, scripts and the cut table: ~/.claude/orchestrate/podnotes/evidence/PERF1/ on the build host.

Release impact

No settings, data.json, API or URI change. fuse.js leaves the dependency list.

One open decision for the owner: main.js now carries a /*! Fuse.js attribution with the Apache 2.0 license URL, and the full license text sits next to the port in the repository. Master shipped Fuse with no notice at all. Apache 2.0 asks redistributors to give recipients a copy of the license, and releases ship only main.js and manifest.json. Embedding the full text in main.js would cost about 3.8 KB gzip, so this PR does not.

Docs at release

None.

Note

Replace Fuse.js with local fuzzySearch and strip CSS comments to shrink main.js by 10%

  • Adds a generic Fuse.js-style search in fuzzySearch.ts and rewires searchEpisodes.ts and TemplateEngine to use it, removing the Fuse.js dependency from package.json
  • Adds a PostCSS-based stripCssComments utility in css-comments.ts, wired into the Svelte style preprocessor in vite.config.ts; comments starting with ! are kept
  • Vite output now suppresses pure annotation comments via Rollup config
  • Compatibility is pinned to Fuse.js 7.5.0 ordering via recorded fixtures in fuse-7.5.0.json and a regenerable generate.mjs script; the ported code carries an Apache 2.0 notice in fuzzySearch.LICENSE
  • Risk: searchEpisodes drops its WeakMap cache and signature checks; search ordering could drift from Fuse.js if the local scoring differs — the fixture tests are the guard

Macroscope summarized 57ddfbb.

main.js is the final artifact, so the 376 /* @__PURE__ */ and
__NO_SIDE_EFFECTS__ comments the minifier kept only cost bytes. The
parsed program is identical to before. main.js goes from 99,975 to
99,496 gzip bytes.
Svelte copies style-block comments verbatim into the CSS it injects
from main.js, so every design note cost every user bytes. A style
preprocessor now drops them at build time and keeps /*! license
notices, so the notes stay in source. Outside the CSS strings the
bundle is unchanged, and the CSS differs only in comments and
whitespace. main.js goes from 99,496 to 98,463 gzip bytes.
Every engine Obsidian runs on supports unprefixed animation, transform
and box-sizing, so the -webkit-, -moz- and -o- copies and the three
prefixed @Keyframes never applied. In Obsidian the old and new rules
give identical computed styles and keyframes for every bar. main.js
goes from 98,463 to 98,297 gzip bytes.
PodNotes used fuse.js only for plain fuzzy matching of episode titles
and template tag names. This ports that search path from Fuse.js 7.5.0
and drops the dependency, along with the per-list index cache that a
port without an index no longer needs.

A differential run against fuse.js 7.5.0 matched every result list in
405,000 comparisons. Searching 2,000 titles per keystroke got faster
(median 4.6 ms against 5.8 ms). main.js goes from 98,297 to 89,692 gzip
bytes.
The style preprocessor removed anything shaped like a comment,
including text inside quoted strings such as content: "/* */". It now
matches strings first and keeps them. Today's styles have no such
strings, so the injected CSS is unchanged.
…notice

fuzzySearch now takes the items and a text accessor and returns the
matches, so neither caller maps indices back. Its Fuse.js attribution is
a /*! notice that survives minification into main.js, and the Apache
License 2.0 text sits next to the port.
The fixture records fuse.js 7.5.0 results for 150 title queries (14
longer than 64 characters) and 80 tag queries, with the options
PodNotes used. Dropping the field norm, flipping findAllMatches,
ignoring later pattern chunks, or keeping the best hit's score instead
of the last makes it fail. The cache-era in-place mutation test goes,
since nothing caches results anymore.
The stripper skipped /*! notices without consuming them, so a quote or
a /* inside a notice changed what came after it. It now matches every
comment whole and keeps notices, and a string continued across a CRLF
line break stays one string.
Callers pass { findAllMatches } as they did to Fuse, instead of a bare
boolean. The shipped notice points at the license URL, since users
never receive fuzzySearch.LICENSE.
The fixture now covers a title listed after a longer sibling ("Rents",
then "Rent") and several titles that share a 49-character prefix, so
dropping the exact-match shortcut or the per-chunk score average fails
it. The generator and the options it used are in the repository, and
the test checks how many queries the fixture holds.
The generator resolves the fuse.js path from the working directory, and
its header fetches fuse.js into /tmp and formats the output. The corpus
gains a short title that a long query's tail chunk can match, so
dropping the chunk location clamp also fails the fixture. Extra seed
queries are a parameter instead of a check on which corpus is passed.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: 57ddfbb
Status: ✅  Deploy successful!
Preview URL: https://d6aca9d5.podnotes.pages.dev
Branch Preview URL: https://perf-bundle-diet.podnotes.pages.dev

View logs

The regex removed text inside an unquoted url() and joined tokens that a
comment separated, so one/**/two became onetwo. PostCSS, already in the
toolchain through Vite, tokenizes the CSS and represents only comments
between rules and declarations as nodes. Those are removed, license
notices stay, and comments inside selectors or values are left alone.
For today's components the injected CSS is unchanged apart from the
class hashes. main.js goes from 89,839 to 89,833 gzip bytes.
@chhoumann
chhoumann merged commit 3980111 into master Oct 12, 2026
11 checks passed
@chhoumann
chhoumann deleted the perf/bundle-diet branch October 12, 2026 15:58
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