fix: work around Chrome GPU bug causing severe lag when dragging links (#7394)

## 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)
This commit is contained in:
Terry Jia
2025-12-11 20:38:00 -05:00
committed by GitHub
parent 03e9dd4789
commit 91adcaf276

View File

@@ -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) {