Canvas绘制耗时过长致数据请求延迟的解决方案咨询
Canvas全量绘制导致请求延迟的解决方案咨询
运行的函数会循环从服务器获取数据并绘制到Canvas上,测试发现Canvas绘制环节耗时最长——因需遍历全量数据集绘制每个元素以展示数据概览,这导致后续请求延迟,出现大量数据获取空档。现针对以下两个方案咨询可行性与实现方式:
1. 首选方案:将Canvas绘制流程放到独立后台线程执行
可行性
完全可行,JavaScript中通过Web Worker实现后台线程计算,主线程专注于数据请求和最终渲染,适合可视化滞后不影响业务的场景。
实现方式
Web Worker无法直接操作DOM/Canvas上下文,核心思路是把绘制所需的计算逻辑放到Worker中,生成可直接渲染的ImageData后传回主线程:
- 步骤1:创建Worker脚本(如
draw-worker.js),在其中处理所有绘制相关的坐标计算、渐变生成、矩形绘制逻辑,最终生成ImageData对象。 - 步骤2:主线程初始化Worker,每次获取到新数据后,将
Logs、MeasurementPeriods、Canvas尺寸等必要参数发送给Worker。 - 步骤3:主线程监听Worker的消息回调,收到
ImageData后,通过Canvas上下文的putImageData方法直接渲染到页面Canvas上。
主线程代码示例
// 初始化Worker const drawWorker = new Worker('draw-worker.js'); // 数据获取完成后发送给Worker function onDataReceived(Logs, MeasurementPeriods) { drawWorker.postMessage({ Logs, MeasurementPeriods, canvasWidth: overviewCanvas.width, canvasHeight: overviewCanvas.height, scaleRatio, startEvent, endEvent, continueEvent, devicePixelRatio: window.devicePixelRatio }); } // 接收Worker返回的ImageData并渲染 drawWorker.onmessage = (e) => { const imageData = e.data; overviewContext.putImageData(imageData, 0, 0); };
Worker脚本(draw-worker.js)示例
self.onmessage = (e) => { const { Logs, MeasurementPeriods, canvasWidth, canvasHeight, scaleRatio, startEvent, endEvent, continueEvent, devicePixelRatio } = e.data; // 创建离屏Canvas(Worker中可使用OffscreenCanvas) const offscreenCanvas = new OffscreenCanvas(canvasWidth, canvasHeight); const overviewContext = offscreenCanvas.getContext('2d'); // 复用原绘制逻辑 overviewContext.clearRect(0, 0, canvasWidth, canvasHeight); overviewContext.globalAlpha = 0.45; let startY = 0; let startX = 0; let grd = 0; const scale = 5; const measurementDuration = ((MeasurementPeriods[MeasurementPeriods.length - 1].end - MeasurementPeriods[0].start) / 1000) const xscale = (canvasWidth / devicePixelRatio) / measurementDuration; overviewContext.beginPath(); for (let i = 1; i < Logs.length; i++) { if (Logs[i].event == startEvent) { startY = ((canvasHeight / scaleRatio) - 10) - (scale * Logs[i].block_ident); startX = ((Logs[i].timestamp / 1000) * xscale); grd = overviewContext.createLinearGradient(startX, startY, startX, startY + scale); grd.addColorStop(0, getStartCol(Logs[i].block_ident)); grd.addColorStop(1, getEndCol(Logs[i].block_ident)); overviewContext.fillStyle = grd; overviewContext.fillRect(startX, startY, ((Logs[i].durationWithoutInterrupt / 1000) * xscale), scale); if ((i + 1) >= Logs.length) { overviewContext.fillRect(startX, startY, ((Logs[i].durationWithInterrupt / 1000) * xscale), scale); } else { if (Logs[i + 1].event == endEvent) { overviewContext.fillRect(startX, startY, ((Logs[i].durationWithoutInterrupt / 1000) * xscale), scale); } else { overviewContext.fillRect(startX, startY, ((Logs[i].durationWithInterrupt / 1000) * xscale), scale); } } } if (Logs[i].event == continueEvent) { startY = ((canvasHeight / scaleRatio) - 10) - (scale * Logs[i].block_ident); startX = ((Logs[i].timestamp / 1000) * xscale); grd = overviewContext.createLinearGradient(startX, startY, startX, startY + scale); grd.addColorStop(0, getStartCol(Logs[i].block_ident)); grd.addColorStop(1, getEndCol(Logs[i].block_ident)); overviewContext.fillStyle = grd; overviewContext.fillRect(startX, startY, ((Logs[i].durationWithInterrupt / 1000) * xscale), scale); overviewContext.fill(); } } for (let i = 0; i < MeasurementPeriods.length - 1; i++) { overviewContext.globalAlpha = 0.75; const startY = ((canvasHeight / scaleRatio) - 10) - (scale * (Logs[i]?.block_ident || -1)); const startX = (((MeasurementPeriods[i].end - MeasurementPeriods[0].start) / 1000) * xscale); const grd = overviewContext.createLinearGradient(startX, startY, startX, startY + scale); grd.addColorStop(0, 'rgb(255,0,0)'); grd.addColorStop(1, 'rgb(255,150,150)'); overviewContext.fillStyle = grd; overviewContext.beginPath(); const width = (((MeasurementPeriods[i + 1].start - MeasurementPeriods[i].end) / 1000) * xscale); overviewContext.fillRect(startX, startY, width, scale); } // 生成ImageData并传回主线程 const imageData = overviewContext.getImageData(0, 0, canvasWidth, canvasHeight); self.postMessage(imageData); }; // 复制原有的getStartCol、getEndCol函数到Worker中 function getStartCol(blockIdent) { // 原函数逻辑 } function getEndCol(blockIdent) { // 原函数逻辑 }
2. 方案2:仅绘制新增数据+缩放已有内容
可行性
可行,但需注意多次缩放导致的画质损失问题,适合对画质要求不高或可通过逻辑优化弥补损失的场景。
实现方式
核心思路是不再全量重绘,通过临时Canvas缩放已有内容,再叠加新增数据:
- 计算缩放比例:每次新数据到来时,重新计算
xscale(全量数据时间范围变大,需调整横向缩放比例)。 - 缩放已有内容:创建临时Canvas,将原Canvas内容绘制到临时Canvas并应用缩放变换,再将缩放后的内容绘制回原Canvas。
- 绘制新增数据:计算新增数据的坐标,在原Canvas对应位置绘制新元素。
代码示例
// 记录上次绘制到的Logs索引 let lastRenderedIndex = 0; function drawIncrementalChart(newLogs, MeasurementPeriods) { const measurementDuration = ((MeasurementPeriods[MeasurementPeriods.length - 1].end - MeasurementPeriods[0].start) / 1000); const newXscale = (overviewCanvas.width / window.devicePixelRatio) / measurementDuration; const oldXscale = (overviewCanvas.width / window.devicePixelRatio) / ((MeasurementPeriods[MeasurementPeriods.length - 2]?.end - MeasurementPeriods[0].start || 0) / 1000); // 缩放已有内容 if (oldXscale > 0 && newXscale !== oldXscale) { const tempCanvas = document.createElement('canvas'); tempCanvas.width = overviewCanvas.width; tempCanvas.height = overviewCanvas.height; const tempCtx = tempCanvas.getContext('2d'); // 缩放绘制原Canvas内容到临时Canvas tempCtx.scale(newXscale / oldXscale, 1); tempCtx.drawImage(overviewCanvas, 0, 0); // 清空原Canvas,绘制缩放后的内容 overviewContext.clearRect(0, 0, overviewCanvas.width, overviewCanvas.height); overviewContext.drawImage(tempCanvas, 0, 0); } // 绘制新增的Logs数据 overviewContext.globalAlpha = 0.45; const scale = 5; const xscale = newXscale; for (let i = lastRenderedIndex; i < newLogs.length; i++) { if (newLogs[i].event == startEvent) { const startY = ((overviewCanvas.height / scaleRatio) - 10) - (scale * newLogs[i].block_ident); const startX = ((newLogs[i].timestamp / 1000) * xscale); const grd = overviewContext.createLinearGradient(startX, startY, startX, startY + scale); grd.addColorStop(0, getStartCol(newLogs[i].block_ident)); grd.addColorStop(1, getEndCol(newLogs[i].block_ident)); overviewContext.fillStyle = grd; overviewContext.fillRect(startX, startY, ((newLogs[i].durationWithoutInterrupt / 1000) * xscale), scale); if ((i + 1) >= newLogs.length) { overviewContext.fillRect(startX, startY, ((newLogs[i].durationWithInterrupt / 1000) * xscale), scale); } else { if (newLogs[i + 1].event == endEvent) { overviewContext.fillRect(startX, startY, ((newLogs[i].durationWithoutInterrupt / 1000) * xscale), scale); } else { overviewContext.fillRect(startX, startY, ((newLogs[i].durationWithInterrupt / 1000) * xscale), scale); } } } // continueEvent的绘制逻辑同理 } // 更新已绘制索引 lastRenderedIndex = newLogs.length; // 绘制MeasurementPeriods的新增部分(逻辑同上) }
原绘制代码
function drawScrollChart(Logs) { overviewContext.clearRect(0, 0, overviewCanvas.width, overviewCanvas.height); overviewContext.globalAlpha = 0.45; var startY = 0; var startX = 0; var grd = 0; var scale = 5; var measurementDuration = ((MeasurementPeriods[MeasurementPeriods.length - 1].end - MeasurementPeriods[0].start) / 1000) var xscale = (overviewCanvas.width / window.devicePixelRatio) / measurementDuration; overviewContext.beginPath(); for (var i = 1; i < Logs.length; i++) { if (Logs[i].event == startEvent) { startY = ((overviewCanvas.height / scaleRatio) - 10) - (scale * block_ident); startX = ((Logs[i].timestamp / 1000) * xscale); grd = overviewContext.createLinearGradient(startX, startY, startX, startY + scale); grd.addColorStop(0, getStartCol(block_ident)); grd.addColorStop(1, getEndCol(block_ident)); overviewContext.fillStyle = grd; overviewContext.fillRect(startX, startY, ((Logs[i].durationWithoutInterrupt / 1000) * xscale), scale); if ((i + 1) >= Logs.length) { overviewContext.fillRect(startX, startY, ((Logs[i].durationWithInterrupt / 1000) * xscale), scale); } else { if (Logs[i + 1].event == endEvent) { overviewContext.fillRect(startX, startY, ((Logs[i].durationWithoutInterrupt / 1000) * xscale), scale); } else { overviewContext.fillRect(startX, startY, ((Logs[i].durationWithInterrupt / 1000) * xscale), scale); } } } if (Logs[i].event == continueEvent) { startY = ((overviewCanvas.height / scaleRatio) - 10) - (scale * block_ident); startX = ((Logs[i].timestamp / 1000) * xscale); grd = overviewContext.createLinearGradient(startX, startY, startX, startY + scale); grd.addColorStop(0, getStartCol(block_ident)); grd.addColorStop(1, getEndCol(block_ident)); overviewContext.fillStyle = grd; overviewContext.fillRect(startX, startY, ((Logs[i].durationWithInterrupt / 1000) * xscale), scale); overviewContext.fill(); } } for (var i = 0; i < MeasurementPeriods.length - 1; i++) { overviewContext.globalAlpha = 0.75; var startY = ((overviewCanvas.height / scaleRatio) - 10) - (scale * block_ident); var startX = (((MeasurementPeriods[i].end - MeasurementPeriods[0].start) / 1000) * xscale); var grd = overviewContext.createLinearGradient(startX, startY, startX, startY + scale); grd.addColorStop(0, 'rgb(255,0,0)'); grd.addColorStop(1, 'rgb(255,150,150)'); overviewContext.fillStyle = grd; overviewContext.beginPath(); var width = (((MeasurementPeriods[i + 1].start - MeasurementPeriods[i].end) / 1000) * xscale); overviewContext.fillRect(startX, startY, width, scale); } }
数据集样本
{ "block_ident": -1, "event": "e", "timestamp": 0, "durationWithInterrupt": 0, "durationWithoutInterrupt": 0 }, { "block_ident": 1, "event": "s", "timestamp": 0, "durationWithInterrupt": 16640, "durationWithoutInterrupt": 16640 }, { "block_ident": 1, "event": "e", "timestamp": 16640, "durationWithInterrupt": 0, "durationWithoutInterrupt": 0 }, { "block_ident": -1, "event": "s", "timestamp": 16640, "durationWithInterrupt": 1132288, "durationWithoutInterrupt": 1132288 }, { "block_ident": -1, "event": "e", "timestamp": 1148928, "durationWithInterrupt": 0, "durationWithoutInterrupt": 0 }, { "block_ident": 3, "event": "s", "timestamp": 1148928, "durationWithInterrupt": 24064, "durationWithoutInterrupt": 24064 }, { "block_ident": 3, "event": "e", "timestamp": 1172992, "durationWithInterrupt": 0, "durationWithoutInterrupt": 0 }
内容的提问来源于stack exchange,提问作者CodeStallion
相关产品推荐
相关产品推荐

