Skip to content

Typing becomes slow with a very tall image attached to the composer #10361

Description

@MaximilianMauroner

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:

  • Dimensions: 2304 × 32766 pixels, approximately 75.5 megapixels.
  • PNG file size: 5,967,750 bytes.
  • Estimated decoded RGBA size: 288 MiB, before any additional browser or GPU copies.

The original image and all personal data are excluded from this report.

Reproduction

  1. Create a synthetic PNG with dimensions 2304 × 32766. Use varied content so the compressed file is approximately 6 MB. A solid-color PNG can test decoded-image pressure but will not reproduce the same storage cost.
  2. Open a new thread and attach the PNG.
  3. Type continuously, then type with pauses longer than 300 ms.
  4. Observe whether characters appear late or typing stalls.
  5. Compare with a draft without attachments and with a resized copy of the same synthetic image.

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:

  1. 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.

  2. 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, through JSON.stringify. In lib/storage.ts, createDeferredStorage writes it to synchronous localStorage after 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

  • Generate a bounded-resolution thumbnail for attachment previews and keep the original separately for sending.
  • Store attachment blobs separately from prompt text, preferably in asynchronous storage or the existing attachment-upload system.
  • Persist attachment references with the draft so prompt edits do not serialize the full image again.
  • Keep thumbnail generation and image conversion out of the per-keystroke render path.
  • Use a targeted renderer trace to determine whether decoding, painting, serialization, or storage dominates.

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:

  • Typing does not repeatedly decode the original or rewrite its base64 payload.
  • Input latency remains close to the no-attachment case.
  • Draft text and attachments survive navigation and restart.
  • Sending still uses the original image.

Prepared with GPT-6 using Codex.

Activity

  1. juliusmarminge commented on Sep 6, 2026

    @juliusmarminge
    Member

    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.tsx the 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, previewUrl is URL.createObjectURL(attachmentFile) after prepareImageForAttachment. 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() sets previewUrl to the full base64 dataUrl, 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 ChatComposer reads each file with readFileAsDataUrl and stores it on persistedAttachments[].dataUrl. partializeComposerDraftStoreState writes those attachments into the persisted store. createDeferredStorage (lib/storage.ts) JSON.stringifys the whole store and writes synchronous localStorage after 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

    Suggested fix

    1. Generate a bounded-resolution thumbnail for composer chips; keep the original File for send/upload.
    2. Persist attachment references (upload id / blob / IndexedDB), not base64, so prompt edits do not re-stringify the image. Match how file attachments already work.
    3. Consider a pixel budget in prepareImageForAttachment for PNG/JPEG.
    4. 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.

  2. added
    via-triageFiled through npx t3 triage
    bugSomething is broken or behaving incorrectly.
    on Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions