Skip to content

Font assets are eagerly imported, so read-only consumers bundle 2.79 MiB they cannot use #61

Description

@fcbwilliams

Summary

Every vendored font asset is eagerly reachable from the module graph, so a consumer that only ever
reads PDFs still bundles ~2.79 MiB of font binaries it can never execute. On Cloudflare Workers,
where the free-plan limit is 3 MB gzipped for the entire Worker, this is the difference between the
package being adoptable and not.

Measurement

documents.js@1.100.1 (which depends on pdf-codec@2.2.32) installed clean, single entry point
importing only pdfToMarkdown, bundled with esbuild:

esbuild entry.mjs --bundle --minify --format=esm --platform=browser --target=es2023
minified gzipped
as published 3.83 MiB 2.17 MiB
with dist/assets/*.js stubbed to empty Uint8Array exports 1113 KiB 300 KiB

Per-module attribution from esbuild's metafile:

1052 KiB  pdf-codec/dist/assets/stix-two-math-font.js
 430 KiB  pdf-codec/dist/assets/carlito-bolditalic.js
 375 KiB  pdf-codec/dist/assets/carlito-italic.js
 374 KiB  pdf-codec/dist/assets/carlito-bold.js
 358 KiB  pdf-codec/dist/assets/carlito-regular.js
  57 KiB  pdf-codec/dist/assets/caladea-bolditalic.js
  56 KiB  pdf-codec/dist/assets/caladea-italic.js
  55 KiB  pdf-codec/dist/assets/caladea-bold.js
  54 KiB  pdf-codec/dist/assets/caladea-regular.js
  50 KiB  pdf-codec/dist/afm-widths-*.js
--------
2790 KiB  fonts + metrics
1063 KiB  all actual code

So ~72% of the bundle is font data, on a path that never writes a PDF.

Cause

dist/font-registry.js and dist/math-font.js are the two modules that reference the assets, and
both import them eagerly at module scope. Because they sit on the same graph as the read path, the
assets survive tree-shaking even though sideEffects: false is set correctly and the consumer
imports one read-only function.

Deep-importing does not help — I tried routing around the barrel and got a byte-identical 2.17 MiB,
because the write path and read path share a module.

Ask

Make the font assets severable from the read path, by whichever of these fits the design best:

  1. Lazy asset importsawait import() the face inside the resolver that needs it, so a
    caller that never renders text never pulls the bytes. Changes the affected functions to async,
    which may be unacceptable; noting it for completeness rather than recommending it.
  2. A parse-only entry point — e.g. pdf-codec/read, whose graph excludes font-registry and
    math-font entirely. Cheapest option, no API change for existing callers.
  3. Assets as a separate optional package@pdf-codec/fonts or similar, resolved through an
    injectable font provider. Most invasive, but also makes "bring your own faces" a first-class
    feature rather than a special case.

Option 2 alone gets a read-only consumer from 2.17 MiB to ~300 KiB gzipped, which is the outcome
that matters here.

Why it matters

A PDF-to-text/Markdown extraction path on Cloudflare Workers is a natural use of this package, and
300 KiB is a comfortable fit in that budget while 2.17 MiB is not. Reproducible entirely from the
published npm packages — no private code involved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions