From 0c7d7214aebbe5aa02789c884961066ed1c6ba33 Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Wed, 4 Nov 2026 18:29:15 +0530 Subject: [PATCH 1/2] perf(web): avoid downloading the HEIC decoder twice --- .../src/components/files/imageDecode.worker.ts | 15 +++++++++++++-- apps/web/src/lib/heicDecoder.ts | 6 ++++++ apps/web/src/lib/imageCompression.test.ts | 4 ++-- apps/web/src/lib/imageCompression.ts | 3 ++- apps/web/vite.config.ts | 1 - 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/lib/heicDecoder.ts diff --git a/apps/web/src/components/files/imageDecode.worker.ts b/apps/web/src/components/files/imageDecode.worker.ts index cf3c9c121f9be..20df38f0f23c4 100644 --- a/apps/web/src/components/files/imageDecode.worker.ts +++ b/apps/web/src/components/files/imageDecode.worker.ts @@ -1,3 +1,5 @@ +import { loadHeicDecoder } from "../../lib/heicDecoder"; + const MAX_PIXELS = 32_000_000; function validateTiffMetadata(bytes: ArrayBuffer) { @@ -152,8 +154,17 @@ async function decode(bytes: ArrayBuffer, heif: boolean) { if (bytes.byteLength > 50 * 1024 * 1024) throw new Error("Preview limited to files under 50 MB."); if (heif) { validateHeif(bytes); - const { heicTo } = await import("heic-to/next"); - return await heicTo({ blob: new Blob([bytes]), type: "image/png" }); + const { heicTo } = await loadHeicDecoder(); + const bitmap = await heicTo({ blob: new Blob([bytes]), type: "bitmap" }); + try { + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + const context = canvas.getContext("2d"); + if (!context) throw new Error("Image decoding is unavailable."); + context.drawImage(bitmap, 0, 0); + return await canvas.convertToBlob({ type: "image/png" }); + } finally { + bitmap.close(); + } } const { decode: decodeTiff } = await import("tiff"); validateTiffMetadata(bytes); diff --git a/apps/web/src/lib/heicDecoder.ts b/apps/web/src/lib/heicDecoder.ts new file mode 100644 index 0000000000000..5dc6744529fac --- /dev/null +++ b/apps/web/src/lib/heicDecoder.ts @@ -0,0 +1,6 @@ +import decoderUrl from "heic-to/csp?url"; + +// Emit one unchanged module so the main and worker builds share the same decoder asset. +export function loadHeicDecoder(): Promise { + return import(/* @vite-ignore */ decoderUrl); +} diff --git a/apps/web/src/lib/imageCompression.test.ts b/apps/web/src/lib/imageCompression.test.ts index e40c3968bc850..cc0a069214ff4 100644 --- a/apps/web/src/lib/imageCompression.test.ts +++ b/apps/web/src/lib/imageCompression.test.ts @@ -19,8 +19,8 @@ const mocks = vi.hoisted(() => ({ heicTo: vi.fn(), })); -vi.mock("heic-to/csp", () => ({ - heicTo: mocks.heicTo, +vi.mock("./heicDecoder", () => ({ + loadHeicDecoder: async () => ({ heicTo: mocks.heicTo }), })); const originalCreateImageBitmap = globalThis.createImageBitmap; diff --git a/apps/web/src/lib/imageCompression.ts b/apps/web/src/lib/imageCompression.ts index 53e948e535b75..a7038b375258d 100644 --- a/apps/web/src/lib/imageCompression.ts +++ b/apps/web/src/lib/imageCompression.ts @@ -1,3 +1,4 @@ +import { loadHeicDecoder } from "./heicDecoder"; import { downscalableImageMimeType, fitImageAttachmentSize, @@ -510,7 +511,7 @@ async function convertHeicImage( ? await createImageBitmap(file).catch(() => undefined) : undefined; if (!bitmap) { - const { heicTo } = await import("heic-to/csp"); + const { heicTo } = await loadHeicDecoder(); bitmap = await heicTo({ blob: file, type: "bitmap" }); } const imageSize = fitImageAttachmentSize(bitmap) ?? { diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 63f1f1f737ebe..cfa03ee2b0f62 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -122,7 +122,6 @@ const browserTestProject = { "fast-xml-parser", "entities", "dompurify", - "heic-to/next", "jszip", "pdfjs-dist", "sql.js", From 7ba2894b5f37381e8df0914b90db4beb76dd3b19 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Nov 2026 00:04:50 +0000 Subject: [PATCH 2/2] perf(web): release the HEIC bitmap before PNG encoding Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/files/imageDecode.worker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/files/imageDecode.worker.ts b/apps/web/src/components/files/imageDecode.worker.ts index 20df38f0f23c4..b7f09f43951bc 100644 --- a/apps/web/src/components/files/imageDecode.worker.ts +++ b/apps/web/src/components/files/imageDecode.worker.ts @@ -156,15 +156,15 @@ async function decode(bytes: ArrayBuffer, heif: boolean) { validateHeif(bytes); const { heicTo } = await loadHeicDecoder(); const bitmap = await heicTo({ blob: new Blob([bytes]), type: "bitmap" }); + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); try { - const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); const context = canvas.getContext("2d"); if (!context) throw new Error("Image decoding is unavailable."); context.drawImage(bitmap, 0, 0); - return await canvas.convertToBlob({ type: "image/png" }); } finally { bitmap.close(); } + return await canvas.convertToBlob({ type: "image/png" }); } const { decode: decodeTiff } = await import("tiff"); validateTiffMetadata(bytes);