Skip to content

feat(html): load the later chunks of a text file on demand - #1253

Merged
andiwand merged 6 commits into
mainfrom
feat/text-chunks-on-demand
Oct 24, 2026
Merged

andiwand merged 6 commits into
mainfrom
feat/text-chunks-on-demand

Conversation

@andiwand

@andiwand andiwand commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Refs #764.

Summary

This is level 3 of docs/design/lazy-loading.md for text files.

  • HtmlConfig::lazy_loading (default off, needs lazy_layout) writes each chunk after the first as a script of its own, text-chunks/<n>.js. The script puts the lines into the page through odr.chunks.fill(n, html).
  • The page: the first chunk stays in the page, so the first frame needs no request. Each later chunk is an empty div with data-odr-chunk-src. Its min-height holds the height of its lines, so the page does not move when they come.
  • viewport.js adds the script when such a chunk becomes near, or when odr.chunks.show shows it. It removes the script again after it has run.
  • Why a script and not fetch: a script loads over HTTP and from file:// alike, where fetch of a file:// URL is blocked. So one page works in both places:
    • bring_offline writes the chunks beside the page. The view returns them as HtmlResources that render on demand.
    • HttpServer serves them through HtmlService::exists and write, with no new route.
    • A binding that renders from files needs no callback. Its web view only has to let the page read the files beside it, as it already must for linked css and js.
  • Core: the text service keeps the text and the byte offset of each chunk (std::call_once), so a chunk costs in proportion to the chunk.
  • Where it stays inline:
    • An editable render, because its log states the whole text.
    • A page that is not UTF-8, because a script is UTF-8.
  • Known limit: odr.search finds only the chunks that have loaded. It stays an open question in the design doc.
  • Shared code: script_string moved to frontend.hpp, and it now replaces bytes that are not UTF-8 rather than throwing.
  • Bindings: apple, jni and python carry the field.
  • Design doc: decisions 3 and 4 now describe what was built.

Measured with a 1 M-line file:

page first frame
level 1 (#1247) 86 MB 0.85 s
level 3 from file:// 485 KB plus 3,906 scripts of about 24 KB 67 ms

Over HTTP the server answers the page in 4 ms and a chunk in 2 ms. Screenshots show the middle (line 500,001) from file:// and the end (line 1,000,000) over HTTP, both loaded after a scroll.

The reference output changes only in text.css, viewport.js and spreadsheet.js; the last picks up the reviewer's fix from #1250. Pins: public a0861bb, private 48b541d.

Test

  • html.a_text_file_loads_its_later_chunks covers:
    • the first 256 lines inline and the placeholder of chunk 1
    • the chunk resource, and exists for chunk 1 but not chunk 0 or one past the end
    • the mimetype, and the script with its counter reset and last line
    • an editable render that keeps every line inline
  • All 2,532 tests pass. The jni ctest and the 12 python tests pass.
  • test/browser/viewport/chunks.html (6 checks): a far chunk loads nothing, a near one loads its lines and drops its source and its script, and a shown one loads. tests.html still passes 17.

Review fixes

  • Rebased onto main after the squashes of feat(html): add a switch for lazy layout #1251 and fix(html): probe the text size adjustment in a document of its own #1252. The jni test had a conflict with the assertFalse of feat(html): add a switch for lazy layout #1251. The rebase keeps assertFalse for lazyLayout, and also uses it for lazyLoading.
  • chunk_of read any string of digits, so text-chunks/01.js and an index that wrapped past std::size_t also named chunk 1. The path must now be the one that chunk_path writes. The test checks both paths.
  • A known limit: a chunk that has not loaded prints as empty space of its height, because beforeprint cannot wait for a script. The design doc now lists this under the open questions, and it updates its status.
  • CHANGELOG.md has an entry for lazy_loading, with its two limits.
  • The wasm CI failed: the text service read the text once and kept it, but TextFile::edit changes the text under the service. So a render after an edit showed the old text, and the wasm test "edits and saves a plain text file" failed. Now only a page that loads its chunks reads the kept text, and every other render reads the text again. html.a_text_file_renders_the_text_of_an_edit fails without the fix and passes with it. The wasm node tests pass locally (68).
  • A limit that stays: a page that loads chunks still reads the kept text. If a host edits such a file through TextFile::edit, the chunks show the text from before the edit. Such a page is not editable, so this needs a host that edits the file from outside the page.
  • Checked again:
    • 288 C++ tests for html, text, XmlHtml and http pass.
    • test/browser/viewport: tests.html gives 17 checks and chunks.html 6, all with 0 failed.
    • The public txt pages are byte-identical to a0861bb, and both pins descend from the pins on main.

Base automatically changed from fix/viewport-zoom-probe to main October 24, 2026 20:26
andiwand and others added 3 commits October 24, 2026 22:27
This is level 3 of `lazy-loading.md` for text files.
`HtmlConfig::lazy_loading` (default off) writes each chunk after the
first as a script of its own, `text-chunks/<n>.js`. The script puts the
lines into the page through `odr.chunks.fill`. `viewport.js` adds the
script when the chunk becomes near. Until then the chunk holds the
height of its lines.

A script loads over HTTP and from a file alike, so one page works in
both places:

- `bring_offline` writes the chunks beside the page.
- The HTTP server serves them through `exists` and `write`, with no
  route of its own.

The service keeps the text and where each chunk starts in it, so a
chunk costs in proportion to the chunk.

A file of one million lines becomes a page of 485 KB and 3906 scripts.
From `file://` it paints its first frame after 67 ms, against 0.85 s
at level 1. The HTTP server answers the page in 4 ms and a chunk in
2 ms.

An editable render keeps every line in the page, because its log
states the whole text. So does a page that is not UTF-8, because a
script is UTF-8. `odr.search` finds only the chunks that have loaded.

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
`chunk_of` read any string of digits, so `text-chunks/01.js` and an
index that wrapped past the size of `std::size_t` named an existing
chunk too. The path must now be the one that `chunk_path` writes.

Also add `lazy_loading` to the changelog, state in the design doc that a
chunk that has not loaded prints as empty space, and update its status.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
@andiwand
andiwand force-pushed the feat/text-chunks-on-demand branch from 93ad5dc to 949b946 Compare October 24, 2026 20:29
andiwand and others added 3 commits October 24, 2026 22:43
The text service read the text once and kept it. But `TextFile::edit`
changes the text under the service, so a render after an edit showed the
text from before it. The wasm test "edits and saves a plain text file"
failed. Now only a page that loads its chunks reads the kept text, and
every other render reads the text again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
The check against `chunk_path` already refuses a path that is not the
one it writes, so a loop over the digits is not necessary. `from_chars`
also refuses an index that does not fit, where the loop wrapped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
@andiwand
andiwand merged commit da95c16 into main Oct 24, 2026
26 of 27 checks passed
@andiwand
andiwand deleted the feat/text-chunks-on-demand branch October 24, 2026 21:23
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