iT邦幫忙

2025 iThome 鐵人賽

DAY 5
3
Modern Web

從 Canvas 到各式各樣的 Web API 之旅系列 第 5

Day 5 - 已經有 DOM + CSS,為何需要 Canvas!?

  • 分享至 

  • xImage
  •  

🧐 當你想在網頁上做動畫、畫圖表、或遊戲時,第一個問題常是:要用 DOM + CSS?還是直接上 Canvas?

DOM+CSS 在 UI 介面與靜態排版上有絕對優勢,但一旦碰到 大量即時繪製逐像素操作高自由度變形複雜圖層混合即時匯出 等需求時,效能與能力就會遇到結構性的瓶頸。這些正是 Canvas 誕生要解決的問題。


1. 大量即時繪製:DOM+CSS 會有效能瓶頸

如果在 DOM 中放上千個小方塊同時移動,代表就是上千個元素。即使每個元素都只是小小一個 <div>,瀏覽器仍然需要為它們計算位置(Reflow)、維護樣式(Repaint)、進行合成(Composite)。這種情況下,大量元素頻繁觸發動畫,很快就會把效能壓力放大,造成明顯 掉幀

Canvas 的做法完全不同:它只有一個 <canvas> 元素,每一幀只是「清空畫布 → 批量繪製所有小方塊」。對瀏覽器來說,管理成本小得多,因此更能穩定地維持流暢度。這也是為什麼 Canvas 特別適合 遊戲、粒子效果、數據可視化 等需要「大量即時繪製」的場景。

下面的影片就是一個簡單的對照實驗:
第一個是 DOM + CSS,第二個是 Canvas,兩邊都跑相同的邏輯與數量。

下方的 YouTube 影片快速示範對照實驗,歡迎點開觀看

Yes

說明細節:

  1. DOM 用的已經是最佳實務:transform

    • 在 DOM 版本裡,我沒有用 left/top 這類會觸發 reflow/repaint 的屬性,而是使用 transform: translate3d
    • 這樣更新只會進到 Composite 階段,已經是 CSS 動畫裡能做到的最高效做法。
  2. 邏輯速度其實一樣

    • 兩邊的動畫都是用同一份亂數種子(seed),同樣的 vx/vy
    • 每個小方塊的移動速度在邏輯上完全一致
  3. 為什麼看起來 DOM 比較慢?

    • 程式採用「每幀加固定位移」:
      • FPS 較低(例如 DOM 在高負載時) → 一秒鐘能跑的幀數變少 → 每秒累積位移變少 → 看起來就慢
      • FPS 較高(Canvas 比較穩定) → 幀數多 → 一秒累積位移較多 → 看起來就快
    • 不是刻意讓 DOM 動得慢,而是 掉幀造成的視覺差異

結構性瓶頸:主執行緒阻塞 + 記憶體壓力

玩過線上範例的讀者,應該能明顯感受到:DOM + CSS 模式下,網頁會卡頓、按鈕反應延遲、計時器更新不順;而 Canvas 模式則能保持流暢運行。
這背後不只是渲染流程 (Reflow / Repaint / Composite) 的問題,還有兩個結構性瓶頸:

  • 主執行緒阻塞 (Main Thread Blocking)
    瀏覽器的大部分工作(動畫計算、事件處理、UI 更新)都在 主執行緒 上進行。
    一旦同時操作上千個 DOM 元素,就會佔滿主執行緒的時間,導致:

    • 滑鼠點擊、輸入延遲
    • 滾動卡頓、不順暢
    • 整體互動回應變慢

    相比之下,Canvas 雖然也跑在主執行緒,但只需要操作單一 <canvas>,每一幀的開銷集中在繪製,不會被大量 DOM 管理成本拖累。

  • 記憶體壓力 (Memory Overhead)
    每個 DOM 元素背後都有額外的物件與資料結構,例如:

    • JavaScript 對應的物件
    • 樣式計算樹 (Style Tree)
    • 事件掛載點、屬性快取

    幾千個 DOM 元素就意味著幾千份額外的記憶體開銷,也會增加垃圾回收 (GC) 的壓力。
    相比之下,Canvas 只有單一元素,記憶體需求更單純,能避免這種長期壓力。

小結

在上千元素的場景下,DOM 動畫即使用了 transform 這種最佳實務,依然會面臨三重挑戰:

  1. 掉幀 —— 畫面不流暢,是使用者最直接感受到的問題。
  2. 主執行緒被壓爆 —— 短期內導致 UI 更新延遲。
  3. 記憶體負擔沉重 —— 長期造成 GC 頻繁觸發,加劇卡頓。

相比之下,Canvas 的單一畫布、批量繪製模型,則能有效避開這些瓶頸,讓 FPS 保持穩定,互動維持順暢。


簡易程式碼範例 — 讓差異一目瞭然

  • DOM + CSS 動畫核心程式碼(用 transform 移動)
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();
  • Canvas 動畫核心程式碼(清空畫布並重繪)
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();

2. 逐像素操作:DOM+CSS 無法做到

DOM+CSS 本質上是「結構+樣式」,能做的事情是改顏色、改大小、或套現成的濾鏡(filter: blur()grayscale())。但 無法直接改像素內容

Canvas 則允許你 存取畫布的每一個像素

  • getImageData 讀取像素陣列(RGBA)。
  • 修改數值。
  • putImageData 寫回去。

這意味著你能實作任何影像處理演算法,例如:

  • Pixelate(馬賽克)
  • Edge Detection(邊緣偵測)
  • 色彩反轉、顏色通道分離

太棒了~ 可以直接看 Day 4 - Canvas API 進階用法篇 的範例 Filter,剛好對應上述三種例子 🎉

逐像素操作-canvas-原圖

逐像素操作-canvas-pixelate

逐像素操作-canvas-edge

逐像素操作-canvas-grayscale


簡易程式碼範例 — 讓差異一目瞭然

  • CSS 濾鏡
img {
  filter: blur(3px) grayscale(100%);
}
  • Canvas 逐像素操作
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);

3. 高自由度的繪製與變形

DOM+CSS 中,變形(rotatescaleskew)只能套用在「元素」這個單位上。
整個盒子會一起被旋轉或縮放,無法只改其中一部分,也不能跨元素組合操作。

而在 Canvas 中,變形操作(translaterotatescaletransform)是作用在 座標系統 上的:

  • 任何後續繪製的圖形、圖片、文字,都會受到影響。

  • 可以透過 save() / restore() 在不同區塊使用不同座標變換。

  • 搭配 clip() 還能裁切成任意路徑,像 Photoshop 的選取區域一樣靈活。

  • 線上範例網址:Canvas Filter & Sticker Playground

高自由度的繪製與變形

小結

  • DOM+CSS → 物件級變形,只能針對單一元素。
  • Canvas → 座標級變形,能自由控制任何像素的繪製與裁切。

簡易程式碼範例 — 讓差異一目瞭然

  • DOM+CSS 物件級變形(整個元素旋轉縮放)
/* 單一元素整體旋轉與縮放 */
.box {
  width: 100px;
  height: 100px;
  background: #3498db;
  transform: rotate(45deg) scale(1.5);
}
  • Canvas 座標級變形(座標系旋轉縮放後繪製)
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();

4. 複合與混色模式

CSS 中,可以用 mix-blend-mode 做圖層混色,但選項有限、瀏覽器支援也不一致,常常受限於標準與相容性。
這些運算實際上發生在瀏覽器渲染流程的 Composite 階段:也就是把多個繪製好的圖層疊合時,才決定上下層像素如何混合。

CanvasglobalCompositeOperation 更進一步,能在 每一次繪製當下就控制「新內容」如何與「既有像素」合成,提供更完整的控制能力:

  • 混色multiplyscreenoverlay 等,效果類似 Photoshop 圖層。
  • 遮罩 / 裁切destination-insource-over 等,可精準決定哪些像素保留或刪除。
  • 特效:製作剪影、發光、局部透明化等效果。

globalCompositeOperation 內建多種模式,這裡不逐一列舉,可以直接在線上範例體驗,或參考 MDN 的圖解文件

複合與混色模式

小結

  • CSS → mix-blend-mode,在 Composite 階段處理圖層混色,功能有限且相容性受限。
  • Canvas → globalCompositeOperation,在 繪製當下就能決定像素如何合成,應用更靈活,特別適合 設計工具、繪圖 App、特效引擎

簡易程式碼範例 — 讓差異一目瞭然

  • CSS 的 mix-blend-mode(元素混色示意)
<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>
  • Canvas 的 globalCompositeOperation(繪製時混色示意)
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';

5. 即時匯出與持久化

DOM+CSS 世界裡,畫面只是「瀏覽器內部渲染結果」,無法直接轉成圖片。
如果要匯出,通常需要借助外部工具(例如 html2canvas),但結果可能不完整,常會出現樣式缺失或相容性問題。

Canvas 則不同,它本身就是一個像素畫布:

  • 可以隨時呼叫 toDataURL 取得 Base64 圖片字串,或用 toBlob 產生 PNG、JPEG 檔案
  • 匯出的內容就是當前畫布的像素狀態,精準、可靠,不依賴額外套件。
  • 這也是為什麼許多 白板工具、設計軟體、即時塗鴉應用 都基於 Canvas —— 因為它能直接把使用者操作存下來,生成可下載或持久化的結果。

小結

  • DOM+CSS → 只能顯示在畫面上,若要匯出圖片需額外套件,且不一定準確。
  • Canvas → 內建匯出能力 (toDataURL / toBlob),能將畫布像素即時保存成檔案,適合需要 逐像素控制、即時大量繪製、結果持久化 的場景。

👉 歡迎追蹤這個系列,我會從 Canvas 開始,一步步帶你認識更多 Web API 🎯


上一篇
Day 4 - Canvas API 進階用法篇
下一篇
Day 6 - Canvas API 效能最佳化
系列文
從 Canvas 到各式各樣的 Web API 之旅8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言