Skip to content

Documents: read long .docx files about a third faster when opening - #140

Merged
jelveh merged 2 commits into
docs-latency/a-docxfrom
docs-latency/read-pr
Oct 28, 2026
Merged

jelveh merged 2 commits into
docs-latency/a-docxfrom
docs-latency/read-pr

Conversation

@jelveh

@jelveh jelveh commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #139, stacked on it (base: docs-latency/a-docx). This one speeds up reading a .docx when it opens.

Results (Chrome, same 28-page, 1,500-paragraph test document as #139)

#139 this PR
DocxPackage.open (median of 9) 505 ms 325 ms
Open → editable (app) ~1.20 s ~1.00 s
Autosave stall 87 ms 88 ms

The JS reading work dropped from ~315 ms to ~135 ms. What remains is mostly native XML parsing and inflate.

Causes and fixes

  • Element-children lookups. Every child()/children() call copied the live children collection into an array, tens of thousands of times per open. They now walk siblings through helpers in docx-xml.js that keep the same results and exceptions, used across the reader modules.
  • Relationships parsed repeatedly. The main document's relationships part (60 KB with 300 hyperlinks) was parsed seven times per open. The colors, hyperlinks, mail-merge and page-stories readers now share the per-bytes cache from Documents: instant typing and faster opening in long documents #139, and each hyperlink looks its relationship up by Id instead of scanning them all.
  • Numbering:
    • Each level read its paragraph properties twice; now once.
    • The test document has 1,584 levels but only 36 distinct ones. Each distinct level's markup is now read once, and every level still gets its own copy.
  • Paragraph source serialized on demand. Opening serialized each body paragraph's source XML with its inherited namespace declarations, about 3 MB of text, which most sessions never use. Opening now copies the paragraph and its declarations, and serializes the copy when it is first needed. Documents: instant typing and faster opening in long documents #139's idle-time save preparation spreads that out, so the first autosave doesn't stall. This is a separate commit that can be dropped on its own.
  • serializeFragment indexes attributes instead of copying them.

Verification

  • Opening and saving: the opened package model, the saved parts and an edited save are identical for all 2,257 fixture and corpus documents in Chrome.
  • Saving on top of Documents: instant typing and faster opening in long documents #139: checked again with this branch: every document edited and saved twice in Chrome, byte-for-byte identical to main.
  • Unit suite: 10,569 tests pass. The touched tests pass on Node 22. The build passes.
  • New test docx-read-reuse.test.js, built from synthetic markup:
    • repeated numbering levels stay separate objects;
    • preview issues from a repeated level are still reported;
    • duplicate relationship Ids are still rejected;
    • unchanged paragraphs save byte-identically;
    • the deferred source is unaffected by later tree changes;
    • the child helpers match [...node.children].

Not done (would change behavior or need a broader audit)

  • Skipping TextDocument's re-resolution of already-resolved paragraphs: one preview-recovered fixture differs.
  • Native DecompressionStream instead of fflate: its errors on corrupt archives differ.

🤖 Generated with Claude Code

Reading a 3 MB document with 1,500 paragraphs, 176 lists and 300
hyperlinks spent most of its time repeating work. Looking up an
element's children copied the live children collection into an array
(tens of thousands of times: every property of every paragraph, run,
style and list level); the readers now walk siblings, several times
faster. The main document's relationships (60 KB) were parsed again by
the theme colors, hyperlinks, mail merge and header/footer readers, and
each hyperlink scanned every relationship; they are now read once and
indexed. Each list level read its paragraph properties twice, and the
document declares 1,584 levels with only 36 distinct ones: a level's
properties are now read once and each distinct level once, every level
still getting its own copy. Serializing paragraph sources indexes
attributes rather than copying them.

DocxPackage.open on that document in Chrome: 505 ms -> 367 ms (median
of 9). What is read and saved is unchanged: the opened package and its
saved and edited exports are identical for all 2,257 corpus documents.
Opening kept the XML of every body paragraph, so that a save after
other edits writes it unchanged. Each one is serialized with the two
dozen namespace declarations it inherits from the document, about 3 MB
of text for a 1,500-paragraph document, and most of it is never used:
an unedited save writes the original part, and a read-only view saves
nothing. Opening now copies each paragraph and the declarations it
inherits, which later changes to the tree cannot reach, and serializes
the copy when the source is first asked for.

DocxPackage.open on that document in Chrome: 367 ms -> 325 ms (median
of 9). The first save after an edit serializes the paragraphs instead.
The opened package and its saved and edited exports are identical for
all 2,257 corpus documents.
@jelveh
jelveh merged commit 1f16ae0 into docs-latency/a-docx Oct 28, 2026
7 of 15 checks passed
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