🧐 當你想在網頁上做動畫、畫圖表、或遊戲時,第一個問題常是:要用 DOM + CSS?還是直接上 Canvas?
DOM+CSS 在 UI 介面與靜態排版上有絕對優勢,但一旦碰到 大量即時繪製、逐像素操作、高自由度變形、複雜圖層混合 或 即時匯出 等需求時,效能與能力就會遇到結構性的瓶頸。這些正是 Canvas 誕生要解決的問題。
如果在 DOM 中放上千個小方塊同時移動,代表就是上千個元素。即使每個元素都只是小小一個 <div>
,瀏覽器仍然需要為它們計算位置(Reflow)、維護樣式(Repaint)、進行合成(Composite)。這種情況下,大量元素頻繁觸發動畫,很快就會把效能壓力放大,造成明顯 掉幀。
Canvas 的做法完全不同:它只有一個 <canvas>
元素,每一幀只是「清空畫布 → 批量繪製所有小方塊」。對瀏覽器來說,管理成本小得多,因此更能穩定地維持流暢度。這也是為什麼 Canvas 特別適合 遊戲、粒子效果、數據可視化 等需要「大量即時繪製」的場景。
下面的影片就是一個簡單的對照實驗:
第一個是 DOM + CSS,第二個是 Canvas,兩邊都跑相同的邏輯與數量。
下方的 YouTube 影片快速示範對照實驗,歡迎點開觀看
DOM 用的已經是最佳實務:transform
left/top
這類會觸發 reflow/repaint 的屬性,而是使用 transform: translate3d
。邏輯速度其實一樣
seed
),同樣的 vx/vy
。為什麼看起來 DOM 比較慢?
玩過線上範例的讀者,應該能明顯感受到:DOM + CSS 模式下,網頁會卡頓、按鈕反應延遲、計時器更新不順;而 Canvas 模式則能保持流暢運行。
這背後不只是渲染流程 (Reflow / Repaint / Composite) 的問題,還有兩個結構性瓶頸:
主執行緒阻塞 (Main Thread Blocking)
瀏覽器的大部分工作(動畫計算、事件處理、UI 更新)都在 主執行緒 上進行。
一旦同時操作上千個 DOM 元素,就會佔滿主執行緒的時間,導致:
相比之下,Canvas 雖然也跑在主執行緒,但只需要操作單一 <canvas>
,每一幀的開銷集中在繪製,不會被大量 DOM 管理成本拖累。
記憶體壓力 (Memory Overhead)
每個 DOM 元素背後都有額外的物件與資料結構,例如:
幾千個 DOM 元素就意味著幾千份額外的記憶體開銷,也會增加垃圾回收 (GC) 的壓力。
相比之下,Canvas 只有單一元素,記憶體需求更單純,能避免這種長期壓力。
在上千元素的場景下,DOM 動畫即使用了 transform
這種最佳實務,依然會面臨三重挑戰:
相比之下,Canvas 的單一畫布、批量繪製模型,則能有效避開這些瓶頸,讓 FPS 保持穩定,互動維持順暢。
const domBox = document.getElementById('dom-box');
let posX = 0, speed = 2;
function animateDOM() {
posX += speed;
if (posX > window.innerWidth - 50 || posX < 0) speed = -speed;
// 只改 transform,不重排
domBox.style.transform = `translateX(${posX}px)`;
requestAnimationFrame(animateDOM);
}
animateDOM();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let posX = 0, speed = 2;
function animateCanvas() {
// 清空畫布
ctx.clearRect(0, 0, canvas.width, canvas.height);
posX += speed;
if (posX > canvas.width - 50 || posX < 0) speed = -speed;
ctx.fillStyle = '#3498db';
// 畫方塊
ctx.fillRect(posX, 25, 50, 50);
requestAnimationFrame(animateCanvas);
}
animateCanvas();
DOM+CSS 本質上是「結構+樣式」,能做的事情是改顏色、改大小、或套現成的濾鏡(filter: blur()
、grayscale()
)。但 無法直接改像素內容。
Canvas 則允許你 存取畫布的每一個像素:
getImageData
讀取像素陣列(RGBA)。putImageData
寫回去。這意味著你能實作任何影像處理演算法,例如:
太棒了~ 可以直接看 Day 4 - Canvas API 進階用法篇 的範例 Filter,剛好對應上述三種例子 🎉
img {
filter: blur(3px) grayscale(100%);
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 馬賽克 Pixelate:每隔 n*n 像素取一次顏色填充區塊
const blockSize = 10;
for (let y = 0; y < canvas.height; y += blockSize) {
for (let x = 0; x < canvas.width; x += blockSize) {
const i = (y * canvas.width + x) * 4;
const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3];
// 填充整個區塊像素為同一顏色 (此示意省略填充細節)
}
}
// 邊緣偵測 Edge:簡單拉普拉斯算子(完整運算邏輯省略)
for (let i = 0; i < data.length; i += 4) {
// 讀取附近像素計算邊緣強度,改變當前像素 RGB(此示意省略詳細演算法)
}
// 灰階 Grayscale:簡單取平均值
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = data[i + 1] = data[i + 2] = avg;
}
ctx.putImageData(imageData, 0, 0);
在 DOM+CSS 中,變形(rotate
、scale
、skew
)只能套用在「元素」這個單位上。
整個盒子會一起被旋轉或縮放,無法只改其中一部分,也不能跨元素組合操作。
而在 Canvas 中,變形操作(translate
、rotate
、scale
、transform
)是作用在 座標系統 上的:
任何後續繪製的圖形、圖片、文字,都會受到影響。
可以透過 save()
/ restore()
在不同區塊使用不同座標變換。
搭配 clip()
還能裁切成任意路徑,像 Photoshop 的選取區域一樣靈活。
/* 單一元素整體旋轉與縮放 */
.box {
width: 100px;
height: 100px;
background: #3498db;
transform: rotate(45deg) scale(1.5);
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 儲存初始座標狀態
ctx.save();
// 平移原點至畫布中心
ctx.translate(canvas.width / 2, canvas.height / 2);
// 旋轉45度 (轉為弧度)
ctx.rotate(45 * Math.PI / 180);
// 縮放1.5倍
ctx.scale(1.5, 1.5);
// 繪製藍色方塊 (以變換後的座標系繪製,中心在原點)
ctx.fillStyle = '#3498db';
ctx.fillRect(-50, -50, 100, 100);
// 恢復原始座標系
ctx.restore();
在 CSS 中,可以用 mix-blend-mode
做圖層混色,但選項有限、瀏覽器支援也不一致,常常受限於標準與相容性。
這些運算實際上發生在瀏覽器渲染流程的 Composite 階段:也就是把多個繪製好的圖層疊合時,才決定上下層像素如何混合。
而 Canvas 的 globalCompositeOperation
更進一步,能在 每一次繪製當下就控制「新內容」如何與「既有像素」合成,提供更完整的控制能力:
multiply
、screen
、overlay
等,效果類似 Photoshop 圖層。destination-in
、source-over
等,可精準決定哪些像素保留或刪除。globalCompositeOperation
內建多種模式,這裡不逐一列舉,可以直接在線上範例體驗,或參考 MDN 的圖解文件。
mix-blend-mode
,在 Composite 階段處理圖層混色,功能有限且相容性受限。globalCompositeOperation
,在 繪製當下就能決定像素如何合成,應用更靈活,特別適合 設計工具、繪圖 App、特效引擎。<div style="position: relative; width: 150px; height: 150px;">
<div style="width: 150px; height: 150px; background: red;"></div>
<!-- 兩個疊在一起的元素,第二個用 multiply 做混色 -->
<div style="width: 150px; height: 150px; background: blue; mix-blend-mode: multiply; position: absolute; top: 0; left: 0;"></div>
</div>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 先畫紅色矩形
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 150, 150);
// 設定混色模式為 multiply
ctx.globalCompositeOperation = 'multiply';
// 再畫藍色矩形,與底色做 multiply 混色
ctx.fillStyle = 'blue';
ctx.fillRect(50, 50, 150, 150);
// 恢復預設混色模式
ctx.globalCompositeOperation = 'source-over';
在 DOM+CSS 世界裡,畫面只是「瀏覽器內部渲染結果」,無法直接轉成圖片。
如果要匯出,通常需要借助外部工具(例如 html2canvas
),但結果可能不完整,常會出現樣式缺失或相容性問題。
Canvas 則不同,它本身就是一個像素畫布:
toDataURL
取得 Base64 圖片字串,或用 toBlob
產生 PNG、JPEG 檔案。toDataURL
/ toBlob
),能將畫布像素即時保存成檔案,適合需要 逐像素控制、即時大量繪製、結果持久化 的場景。👉 歡迎追蹤這個系列,我會從 Canvas 開始,一步步帶你認識更多 Web API 🎯