From 91adcaf276c7654331d62b87adedabe939c25513 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Thu, 11 Dec 2025 20:38:00 -0500 Subject: [PATCH] fix: work around Chrome GPU bug causing severe lag when dragging links (#7394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When Chrome is maximized with GPU acceleration and high DPR, calling drawImage(canvas) + drawImage(img) in the same frame causes severe performance degradation (FPS drops to 2-10, memory spikes ~18GB). Defer image preview rendering using queueMicrotask to separate the two drawImage calls into different tasks. ### Problem Severe performance degradation in ComfyUI when dragging connection lines in litegraph mode: - FPS drops from 60 to 2-10 - Memory spikes from 36GB to 54GB (~18GB increase) - CPU jumps from 2% to 15% - Other Chrome tabs (e.g., YouTube) also stutter ### Environment - Affected: Chrome with GPU acceleration, maximized/fullscreen window, high DPR (1.75) - Not affected: Firefox (WebRender), Chrome in windowed mode, Chrome with GPU acceleration disabled ### Problem only occurs with: - GPU acceleration enabled - Chrome maximized/fullscreen - An image loaded on canvas (e.g., LoadImage node with preview) ### Root cause: The bug is triggered when two drawImage() calls execute in the same frame on the same canvas: - ctx.drawImage(bgcanvas, ...) - copying background canvas to foreground - ctx.drawImage(img, ...) - rendering image preview in node widget ## Screenshots (if applicable) Before https://github.com/user-attachments/assets/76005c10-3430-4d75-a7ed-58f61d18688c After https://github.com/user-attachments/assets/5a15b0f9-3935-4428-879b-e55390abff22 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7394-fix-work-around-Chrome-GPU-bug-causing-severe-lag-when-dragging-links-2c66d73d365081469d73d98bf1aa421a) by [Unito](https://www.unito.io) --- .../composables/useImagePreviewWidget.ts | 72 +++++++++++++++++-- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget.ts b/src/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget.ts index 1b6eeaeeb..304501284 100644 --- a/src/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget.ts +++ b/src/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget.ts @@ -12,6 +12,37 @@ import { calculateImageGrid } from '@/scripts/ui/imagePreview' import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets' import { is_all_same_aspect_ratio } from '@/utils/imageUtil' +/** + * Workaround for Chrome GPU bug: + * When Chrome is maximized with GPU acceleration and high DPR, calling + * drawImage(canvas) + drawImage(img) in the same frame causes severe + * performance degradation (FPS drops to 2-10, memory spikes ~18GB). + * + * Solution: Defer image rendering using queueMicrotask to separate + * the two drawImage calls into different tasks. + * + * Note: As tested, requestAnimationFrame delays rendering to the next frame, + * causing visible image flickering. queueMicrotask executes within the same + * frame, avoiding flicker while still separating the drawImage calls. + */ +let deferredImageRenders: Array<() => void> = [] +let deferredRenderScheduled = false + +function scheduleDeferredImageRender() { + if (deferredRenderScheduled) return + deferredRenderScheduled = true + + queueMicrotask(() => { + const renders = deferredImageRenders + deferredImageRenders = [] + deferredRenderScheduled = false + + for (const render of renders) { + render() + } + }) +} + const renderPreview = ( ctx: CanvasRenderingContext2D, node: LGraphNode, @@ -124,13 +155,31 @@ const renderPreview = ( const imgWidth = ratio * img.width const imgX = col * cellWidth + shiftX + (cellWidth - imgWidth) / 2 - ctx.drawImage( + // Defer image rendering to work around Chrome GPU bug + const transform = ctx.getTransform() + const filter = ctx.filter + const drawParams = { img, - imgX + cell_padding, - imgY + cell_padding, - imgWidth - cell_padding * 2, - imgHeight - cell_padding * 2 - ) + x: imgX + cell_padding, + y: imgY + cell_padding, + w: imgWidth - cell_padding * 2, + h: imgHeight - cell_padding * 2 + } + deferredImageRenders.push(() => { + ctx.save() + ctx.setTransform(transform) + ctx.filter = filter + ctx.drawImage( + drawParams.img, + drawParams.x, + drawParams.y, + drawParams.w, + drawParams.h + ) + ctx.restore() + }) + scheduleDeferredImageRender() + if (!compact_mode) { // rectangle cell and border line style ctx.strokeStyle = '#8F8F8F' @@ -167,7 +216,16 @@ const renderPreview = ( const x = (dw - w) / 2 const y = (dh - h) / 2 + shiftY - ctx.drawImage(img, x, y, w, h) + + // Defer image rendering to work around Chrome GPU bug + const transform = ctx.getTransform() + deferredImageRenders.push(() => { + ctx.save() + ctx.setTransform(transform) + ctx.drawImage(img, x, y, w, h) + ctx.restore() + }) + scheduleDeferredImageRender() // Draw image size text below the image if (allowImageSizeDraw) {