Skip to content

Screenshot capture hangs forever on a below-the-fold lazy image (dialog never reopens) #76

Description

@keenvc

Summary

captureDOM's image-preload step waits on every <img> with no timeout. An <img loading="lazy"> far below the fold is never fetched by the browser, so it fires neither load nor error and complete stays false forever. The Promise.all never settles, the capture promise never resolves, and — because the dialog is set to closed before capture — the reporter dialog never reopens. To the user the widget simply vanishes mid-report, with no error and no way back to the draft.

Where

In the built widget (v1.2.0) this is the image-preload helper:

async function Nm(e){
  const t = e.querySelectorAll("img"), r = [];
  t.forEach(i => {
    i.complete
      ? (i.decode && r.push(i.decode().catch(()=>{})))
      : r.push(new Promise(o => {          // <-- resolves ONLY on load/error
          const s = async () => { /* ... */ o() },
                n = () => { /* ... */ o() };
          i.addEventListener("load", s, {once:true});
          i.addEventListener("error", n, {once:true});
        }))
  });
  r.length > 0 && await Promise.all(r)
}

The dialog is closed before this runs and only reopened from Image.onload / Image.onerror / the catch in the capture handler — none of which are ever reached:

W(!1), f(!0), h("closed"), await new Promise(P=>setTimeout(P,100));
try { const P = await o({...}); /* ... */ it.onload = () => { ... h("form") } }
catch (P) { ... h("form") }

So the failure mode is silent and unrecoverable rather than a caught error with a toast.

Reproduction

  1. Any page taller than the viewport with an <img loading="lazy"> several thousand px below the fold (far enough that the browser does not pre-fetch it).
  2. Open the reporter, go to Screenshots, click Capture Screenshot.
  3. The dialog closes and never comes back. No console error, no toast.error.capture.

Observed on production with Chrome. In the hung state: document.fonts.status === "loaded", 25 images on the page, exactly 1 with complete === false — the lazy one, getBoundingClientRect().top === 3325. Still closed after 70 seconds.

It presents as intermittent because it depends on viewport height: at a 2053px-tall viewport the same image fell inside Chrome's lazy-load threshold, loaded, and capture completed in ~4s; at 1440×810 it hung every time the image was still pending.

Suggested fix

Bound the wait, so a pending image degrades the screenshot rather than deadlocking the widget:

const settled = new Promise(o => { /* load/error as today */ });
r.push(Promise.race([settled, new Promise(o => setTimeout(o, 3000))]));

Two things worth considering alongside it:

  • Force-load pending lazy images before capture (img.loading = "eager" on any !img.complete && img.loading === "lazy", restored afterwards) so they are actually included rather than blank.
  • Guarantee the dialog is restored — e.g. reopen in a finally — so no future hang in the capture path can strand a reporter mid-report.

Happy to send a PR if the shape above looks right.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions