Repository navigation
Typing becomes slow with a very tall image attached to the composer #10361
Description
Activity
Triage
This looks like a real web composer performance bug, not a duplicate of #5626 (that one is large text in the composer). The two paths in the report are present on current
main. They have not been isolated from each other with a remove-image test.What the code does
1. Preview uses the original image. In
ChatComposer.tsxthe resting chip (size-7) and expanded tile (h-16 w-16) both render<img src={image.previewUrl}>. Nothing in that path builds a smaller thumbnail.On attach,
previewUrlisURL.createObjectURL(attachmentFile)afterprepareImageForAttachment. That helper only recompresses when file size exceeds the 10 MiB send cap (PROVIDER_SEND_TURN_MAX_IMAGE_BYTES). A ~6 MB / 75.5 MP PNG passes through unchanged. HEIC has a 64 MP decode cap; PNG/JPEG do not.After reload,
hydrateImagesFromPersisted()setspreviewUrlto the full base64dataUrl, so the 64×64 chip can decode ~288 MiB RGBA from an ~8 MB data URL.2. Draft persistence still embeds image bytes. The persist effect in
ChatComposerreads each file withreadFileAsDataUrland stores it onpersistedAttachments[].dataUrl.partializeComposerDraftStoreStatewrites those attachments into the persisted store.createDeferredStorage(lib/storage.ts)JSON.stringifys the whole store and writes synchronouslocalStorageafter a 300 ms debounce.File attachments already persist metadata only. Images still persist the payload. Prompt edits go through
setPrompt→ persist. #9695 stopped per-keystroke stringify (audit W04 on #9661); a pause still serializes the image-bearing store on the renderer thread.useComposerThreadDraft()returns the whole draft, so every keystroke re-renders the composer, including the<img>.Why “other threads stay responsive” matters
If the global persist write were the main cost, other threads would hitch after pauses >300 ms too. That observation points more at decode/paint of the visible 75 MP preview. Persist can still stall the affected thread after idle. A Performance-panel trace on continuous typing vs pauses >300 ms would settle this; it is not required to accept the bug.
Related work
- [Bug]: Composer becomes extremely laggy/unresponsive with large input content #5626 — same “composer feels dead” symptom, different cause (large text). Related, not a wash duplicate.
- perf(web): defer composer draft serialization #9695 (merged, in
0.0.39-nightly.20260906.1293) — defers stringify; does not remove image bytes from draft JSON or add thumbnails. - perf(clients): fix composer draft persistence boundaries #9049 (open) / perf(mobile): keep image bytes outside draft JSON #9727 (open, mobile, do-not-merge) — keep image bytes out of draft JSON on mobile. Same idea, not a web thumbnail fix.
- feat(web): upload image attachments before sending #8048 (merged) — upload-before-send; a natural place to persist attachment references instead of bytes.
- Track performance audit fixes #9661 W04 is done. This issue is the leftover: still-inlined image bytes + full-res preview.
Suggested fix
- Generate a bounded-resolution thumbnail for composer chips; keep the original
Filefor send/upload. - Persist attachment references (upload id / blob / IndexedDB), not base64, so prompt edits do not re-stringify the image. Match how file attachments already work.
- Consider a pixel budget in
prepareImageForAttachmentfor PNG/JPEG. - Keep thumbnail generation and image conversion off the per-keystroke path.
Validation
Compare no attachment, a normal screenshot, and the 2304×32766 PNG. Check continuous typing and pauses >300 ms. Confirm: no repeated full-image decode or base64 rewrite while typing; latency near the no-attachment case; draft text + attachments survive navigation/restart; send still uses the original.
Severity: medium (major for the affected draft). Workaround: remove or resize the image.
Accepting as
bug.- addedvia-triageFiled through npx t3 triageFiled through npx t3 triagebugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.
on Sep 6, 2026
Problem
Typing in the composer becomes noticeably slow when a very tall PNG is attached. Typing works normally in other threads.
Observed on macOS, T3 Code Nightly
0.0.39-nightly.20260906.1293.The affected image was:
The original image and all personal data are excluded from this report.
Reproduction
For a stronger comparison, test with background tasks idle and record a renderer performance trace during typing.
Expected: Typing stays responsive regardless of attachment dimensions.
Actual: The draft containing the large image has substantial input lag. Other threads remain responsive.
Investigation
Source maps shipped with the affected build show two relevant paths:
The thumbnail uses the original image URL. In
ChatComposer.tsx, the expanded attachment preview renders<img src={image.previewUrl}>inside a 64 × 64 container. This path does not itself generate a smaller thumbnail. The source dimensions create potential decode and rendering pressure despite the small displayed preview.Draft persistence includes the image payload. The affected draft stores the PNG as a base64 data URL, approximately 7.96 MB. In
composerDraftStore.ts, prompt edits update the draft store. Persistence serializes the persisted draft state, including attachments, throughJSON.stringify. Inlib/storage.ts,createDeferredStoragewrites it to synchronouslocalStorageafter a 300 ms debounce.The debounce reduces write frequency, but the serialization and storage write still run on the renderer thread. It does not make those operations asynchronous.
These are plausible causes, not yet isolated by a controlled image-removal test. The persistence path covers the whole draft store, so it could also affect other threads while the image-bearing draft remains saved.
Measurement limits
Diagnostics recorded renderer peaks of 94.1% CPU and 791 MB resident memory during the investigation window. These were not isolated to typing and do not establish causation.
Background directory scans were also active during part of the investigation and could have contributed to overall load. A short native renderer sample did not identify the responsible JavaScript function.
UI automation call timings are not valid measurements of key-to-paint latency and are intentionally omitted.
Suggested fix
Validation
Compare no attachment, a normal screenshot, and the synthetic tall PNG. Check continuous typing and typing with pauses longer than 300 ms.
Verify that:
Prepared with GPT-6 using Codex.