Skip to content

feat(html): lay out only the chunks of a sheet near the viewport - #1250

Merged
andiwand merged 3 commits into
mainfrom
feat/sheet-chunks
Oct 24, 2026
Merged

andiwand merged 3 commits into
mainfrom
feat/sheet-chunks

Conversation

@andiwand

@andiwand andiwand commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Refs #764.

Summary

This implements decision 6 of docs/design/lazy-loading.md (#1249): a sheet is the ruler and a <table> for each chunk of rows. With the 500 K-cell sheet of efficiency-big-1.ods in headless Chrome:

first frame layout renderer RSS
before 7.0 s 2.6 s 2.8 GB
after 4.0 s 1.3 s 2.7 GB

About 2 s of the rest is viewport.js, which existed before this change. It sets the body zoom twice to probe text-size adjustment, and each set restyles and relays out the whole document. That is a follow-up.

Writer (translate_sheet)

  • div.odr-sheet takes the sheet's attributes, and the ruler is a sticky table.odr-sheet-head.
  • A chunk holds up to 256 rows or 4,096 cells, at least 8 rows, and ends only where no rowspan is open.
  • Each chunk states contain-intrinsic-height: auto from its rows' heights, 20px where a row states none.
  • The first chunk of a sheet with shapes is data-odr-chunk-held.
  • Widths: every table states the sheet width (--odr-sheet-width), and the gutter and every column are in px, so table-layout: fixed takes the widths from the cols alone.
    • Without that width, the browser's automatic table layout shrank each column of an over-wide sheet to what its own cells hold, so the ruler and the chunks did not line up. This showed in the 500 K-cell sheet.
    • A column that states no width now gets 7.5px for each character of its longest text in the first 1,000 rows, plus 14px, clamped to 4–60 characters. A cell that wraps counts its longest word.

Scripts

  • spreadsheet.js reads the rows of every chunk.
    • A sort refills the chunks with the same counts.
    • Row edits work in the chunk of the row, and a chunk with no row left goes.
    • Column edits change every table and --odr-sheet-width. A new column is 64px.
    • A reflow in a far chunk waits for the chunk's odr-chunk-near event.
  • viewport.js raises odr-chunk-near, skips held chunks, and gets odr.chunks.show and observe. The editor's place and odr.search show the chunk before they measure or scroll.

References. Every sheet page changes. I checked the change by screenshot A/B:

  • Public: 48 of 83 pages differ by under 0.4% after a blur.
  • Private: 147 of 229.
  • The larger differences are columns that took their content's width before and now take the estimate. Three cases:
    • sample.xlsx: column D is wider.
    • Konzentration.xls: the headers wrap where they did before.
    • ruski.xlsx: a long number in a wide file font spills past its column. That is the limit of an estimate that does not know the font.

Copied to output main with the pins advanced:

  • public fc8d7fb: 83 pages and 6 resources
  • private 434d410: 272 pages and 5 resources

spreadsheet.js and sheet-editing.js also carry earlier PRs' changes that the references had missed.

Test

  • All 2,529 C++ tests pass. The four html tests on sheet style classes now expect the width classes before the cell classes.
  • All 20 pages of test/browser/sheet pass. That includes the new chunks.html (15 checks): positions across chunks, row edits into and out of a chunk, an emptied chunk, a column in every table, a deferred spill, and a sort over chunks. The text, plaintext, viewport, bridge and annotation pages pass too.
  • Screenshots of the 500 K-cell sheet in the middle and of a csv across a chunk boundary: the ruler and the chunks line up.

Review fixes

  • html.a_sheet_ends_a_chunk_only_where_no_merge_is_open: the writer had no C++ test for where a chunk ends. A sheet of 300 rows with a merge over rows 251 to 260 checks that the first chunk takes the merge, and that the second chunk holds the 40 rows left and states 800px.
  • The comment of fods_columns said that a column without a width is exactly its content's width. It now says that such a column takes the width of its longest text.
  • spreadsheet.js declared odr below the helpers that read it. It now comes first.
  • Checked again: 963 C++ tests for html, sheets, text, pdf and edits pass. All pages of test/browser sheet, text, plaintext, viewport, bridge and annotation pass, with the formula pages run against the wasm module.

andiwand and others added 3 commits October 24, 2026 21:39
A sheet is now the ruler and a table for each chunk of rows
(`lazy-loading.md` decision 6), because `content-visibility` has no
effect on a table row. The sheet of 500 K cells in
`efficiency-big-1.ods` took 7.0 s to its first frame in Chrome. Now it
takes 4.0 s.

- A `div.odr-sheet` takes the attributes of the sheet. The ruler is a
  sticky table of its own.
- A chunk holds up to 256 rows or 4096 cells, at least 8 rows, and ends
  only where no merge is open. It states the estimated height of its
  rows. The first chunk of a sheet with shapes stays near.
- Every table states the width of the sheet, so `table-layout: fixed`
  takes the columns from the `col`s alone. A column that states no width
  gets the width of its longest text in its first 1000 rows. A cell that
  wraps counts its longest word.
- `spreadsheet.js` reads the rows of every chunk. A sort puts the rows
  back with the count of each chunk, row edits work in the chunk of the
  row, and column edits change every table and the sheet width. A
  `reflow` in a far chunk waits until `viewport.js` says it is near.
- `viewport.js` gets `odr.chunks.show` and `observe`. The editor and
  `odr.search` show the chunk of a cell before they measure or scroll.

The reference output changes for every sheet page. A column that stated
no width took its content's width before, and now takes the estimate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QtawyvLP2SCnoBtnVfFed
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QtawyvLP2SCnoBtnVfFed
The writer has no C++ test for where a chunk ends. A sheet of 300 rows
with a merge over rows 251 to 260 now checks that the first chunk takes
the merge and the second chunk the 40 rows left, with the heights of both.

Also correct the comment of `fods_columns`: a column without a width
now takes the width of its longest text. In `spreadsheet.js`, declare
`odr` before the helpers that read it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
@andiwand
andiwand merged commit 2bb0f8c into main Oct 24, 2026
36 checks passed
@andiwand
andiwand deleted the feat/sheet-chunks branch October 24, 2026 20:12
andiwand added a commit that referenced this pull request Oct 24, 2026
The views lay out only the parts near the viewport since #1247, #1248 and
#1250, and this PR adds `HtmlConfig::lazy_layout`. A consumer notices
both, but the changelog had no entry. Also check the round trip of the
jni test with `assertFalse`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
andiwand added a commit that referenced this pull request Oct 24, 2026
* feat(html): add a switch for lazy layout

`HtmlConfig::lazy_layout` is on by default. Off, no view writes a chunk
or the style that hides one, so the browser lays out everything. Its
own find and a screen reader then reach every line, row and page. A
host can take that where a file is small.

- The text and sheet views write all their lines or rows as one block.
  The pdf pages get no chunk class.
- `chunk_classes` gives the classes of a chunk, empty where the switch
  is off, and `write_chunk_style` writes nothing then.
- The text styles move from `.odr-chunk` to `.odr-text-lines`, so a
  text page without chunks keeps its line numbers.
- The apple, jni and python bindings carry the switch.

* test: advance the reference output to the lazy layout switch

The design names the switch, and the pins move to the reference output
of the renamed text class and the reordered pdf page classes.

* docs: add the lazy layout and its switch to the changelog

The views lay out only the parts near the viewport since #1247, #1248 and
#1250, and this PR adds `HtmlConfig::lazy_layout`. A consumer notices
both, but the changelog had no entry. Also check the round trip of the
jni test with `assertFalse`.

Co-authored-by: Claude Opus 5.5 (1M context) <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