Skip to content

fix(web): keep chat typing fast beside open PDFs and LaTeX - #801

Merged
yaacovcorcos merged 5 commits into
mainfrom
claude/typing-priority-20261109
Nov 9, 2026
Merged

yaacovcorcos merged 5 commits into
mainfrom
claude/typing-priority-20261109

Conversation

@yaacovcorcos

Copy link
Copy Markdown
Contributor

Platforms: measured and tested on macOS (Apple Silicon) only. The causes are in Chromium, PDF.js and React, which Electron 44 shares on Windows and Linux, but neither was run there. Windows is where #777's author saw the stall.

What Changed

Typing in chat no longer slows down while a document is open. Two root causes are fixed:

  1. An open PDF forced a layout on every keystroke.
    • The cause: on every keyup and selectionchange anywhere in the app, PDF.js's TextLayerBuilder re-appended each text layer's endOfContent, even when it was already last. PDF.js's DrawLayer observes that move and reads Selection.isCollapsed, which forces a synchronous layout.
    • The fix: a one-condition pnpm patch to the legacy viewer Scient imports skips the no-op move. PDF.js main still has the bug.
    • Two Scient selectionchange handlers now read Range.collapsed instead of Selection.isCollapsed.
  2. Typing in a textarea restyled the whole app.
    • The cause: React copies a controlled textarea's value into defaultValue after each change, which replaces its text node. With the app's :has() rules, Chromium then invalidated every element under <body>, about 13,000 with a Visual LaTeX document open.
    • The fix: textareas that never use their native default or form reset now opt in with SKIP_MIRRORED_DEFAULT, and only those skip a default equal to their shown text. That covers Ask in chat (inside a SCIENT-FORK seam) and the six Visual LaTeX textareas. Every other textarea keeps the native behaviour.

Why

This replaces #777. Its PDF.js finding was right and is credited. Measuring showed that the two causes above account for the stalls, so #777's 500 ms input-priority scheduler is not needed. It only deferred the work, and it delayed PDF drawing, panes, previews and clocks with no limit while someone types.

Typing latency per key at 4× CPU throttle. Production build, headless Chromium, 149 keys, Event Timing key-to-paint, p50 / p90 in ms:

Scenario main this PR
Ask in chat, PDF open 416 / 480 16 / 16
Ask in chat, LaTeX open 256 / 280 24 / 24
Ask in chat, no document 56 / 64 16 / 16
Chat, PDF open 144 / 176 16 / 24
Chat, PDF scrolling 288 / 376 24 / 136–168
Chat, no document / source file / LaTeX open 16–32 16–24

Without throttling on an M-series Mac, main reached 24–64 ms in the PDF and Ask cases, and this PR is 16 ms in all of them.

Still slow, and not part of this PR:

  • Scrolling a Visual LaTeX document is slow on its own, with or without typing: about 104 ms per frame unthrottled, from Scient's reading-anchor layout corrections.
  • PDF scrolling keeps some PDF.js-internal forced layouts.

Both deserve their own follow-ups.

Verification

  • New tests, each checked against a negative control:
    • pdfTextLayerReset.browser.test.ts fails against unpatched PDF.js and passes with the patch.
    • textareaDefaultValue.browser.test.tsx's first test fails without the installer. Its second test fails if the skip leaks to unmarked textareas.
    • Both pass at SCIENT_TEST_CPU_THROTTLE=6.
  • vp run check:changed f3354849af passes:
    • unused patches, format, lint and knip;
    • typecheck across 57 packages;
    • 13 affected test files and the React Compiler check.
  • Other local checks pass: alignment:seams:check, latex:seams:check, upstream:provenance:check, and pnpm run test:desktop-smoke in an isolated home.
  • Not run locally: the full test suites and the full build. CI covers them.
  • Codex (gpt-6.1-sol, high):
    • The first review found that an earlier prototype-wide textarea override changed defaultValue/form-reset semantics for every textarea. That's fixed by the opt-in.
    • The re-check found no findings.

Documentation Impact

Documentation impact: Updated — UPSTREAM.md (PDF.js patch and its removal check) and docs/internals/performance-regressions.md (typing beside documents).

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • No visual UI change (latency only)

Implementation: Claude Code (Claude Opus 5.5). Review: Codex (gpt-6.1-sol). Diagnosis credit for the PDF.js listener: nattikahana in #777.

🤖 Generated with Claude Code

yaacovcorcos and others added 3 commits November 9, 2026 14:48
PDF.js's TextLayerBuilder re-appended each text layer's endOfContent on
every keyup and selectionchange in the app, even when it was already the
last child. DrawLayer observes that move and reads Selection.isCollapsed,
which forces a full synchronous layout, so chat typing beside a long PDF
paid for a whole-document layout per key (about 160 ms per key at 4x CPU
throttle with a 320-page PDF; 16 ms after this change).

Patch the legacy viewer Scient imports to skip the move when the node is
already last. Two Scient selectionchange handlers also read
Selection.isCollapsed; they now read the range's collapsed flag, which
does not force layout.

Co-authored-by: nattikahana <93278659+nattikahana@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
React copies a controlled textarea's value into defaultValue after every
change, which replaces the textarea's text node. With the app's :has()
rules, Chromium then invalidates every element under body, so each key in
Ask in chat, the LaTeX source fields and other textareas restyled the
whole document (256 ms per key at 4x with a LaTeX document open; 24 ms
after this change).

Skip a defaultValue write that equals the value the textarea already
shows. Nothing visible changes, and a different default still applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit patched HTMLTextAreaElement.prototype, so every
textarea in the renderer, including library and form textareas, stopped
honouring `defaultValue = value` as a new reset baseline. Scope it:
textareas that never use their native default or form reset spread
SKIP_MIRRORED_DEFAULT, and a focusin listener gives only those elements
the skipping setter. Ask in chat and the Visual LaTeX fields opt in.
Line endings are normalised before comparing, so CRLF drafts are covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Nov 9, 2026
yaacovcorcos and others added 2 commits November 9, 2026 15:28
Reading HTMLTextAreaElement at module load failed every Node unit test
that imports Ask in chat or the LaTeX fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yaacovcorcos
yaacovcorcos added this pull request to the merge queue Nov 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ The exact PR base did not have a successful artifact. Baseline uses the latest successful main measurement shown below.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.5 KiB 5.5 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 4.2 KiB 4.2 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.3 KiB 1.3 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 22.3 KiB 22.3 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.5 KiB 5.5 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 4.2 KiB 4.2 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.3 KiB 1.3 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 22.7 KiB 22.7 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Baseline: 030aca2 · PR result: 3ee80c0 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Bounded compact HTTP snapshot with live cursor.

  • Codex decoded thread snapshot: 88.2 KiB
  • Claude decoded thread snapshot: 88.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant