藥不藥來當工程師一個不務正業的藥師,誤打誤撞來當了前端工程師。斜槓工程師的人生!

用 invisible absolute 解決 Chart.js 在隱藏 Tab 中匯出空白圖片的問題

發佈於 最後更新於
9 min read

最近在做一個多 Tab 頁面的「下載圖表」功能時,踩到一個 Chart.js 的經典坑:當圖表所在的 Tab 被隱藏時,匯出的圖片是空白的。

這篇文章記錄問題的成因、CSS 隱藏方式的差異,以及最終用 invisible absolute 搭配 offscreen 定位解決的過程。

問題背景

頁面結構是一個多 Tab 的分析報告詳情頁,兩個 Tab 分別是「受眾包分析」(包含 Chart.js 圖表)和「內容分析」(純數據表格)。使用者可以透過一個 Modal 選擇下載內容,觸發匯出。

vue
<!-- 原本用 v-show 切換 Tab -->
<ChartPanel v-show="activeTab === 'package_analysis'" :data="data" />

<DataPanel v-show="activeTab === 'content_analysis'" :data="data" />

匯出圖表的核心是 Chart.js 的 toBase64Image() 方法,它會把 Canvas 當前的渲染結果轉成 Base64 PNG。

問題出在:當使用者停留在「內容分析」Tab 時觸發下載,「受眾包分析」Tab 被 v-show 隱藏,匯出的圖片全部是空白的。

為什麼 v-show 會導致空白圖片?

v-show 的底層實作是 display: none1。當一個元素被設為 display: none 時,瀏覽器會將它完全從 Layout 流程中移除——不計算尺寸、不分配空間、不進行繪製。

對 Canvas 來說,這意味著它的寬高會變成 0。Chart.js 的 toBase64Image() 本質上是呼叫 Canvas 的 toDataURL(),而一個 0×0 的 Canvas 自然只能產出空白圖片。

這是 Chart.js 的已知行為,在官方 GitHub Issue #2933 中有明確說明2

CSS 隱藏元素的三種方式比較

要理解解法,先釐清三種常見的 CSS 隱藏方式在瀏覽器渲染管線中的差異:

方式DOM 存在Layout 計算Paint 繪製佔據空間
display: none
visibility: hidden
opacity: 0

關鍵差異在於 Layout 計算3

  • display: none:元素完全從 Layout 中移除,瀏覽器不會計算它的尺寸和位置
  • visibility: hidden:元素仍然參與 Layout,瀏覽器會正常計算尺寸,只是最後不繪製到畫面上
  • opacity: 0:元素完全參與 Layout 和 Paint,只是透明度為 0

對 Canvas 和 Chart.js 來說,我們需要的是「元素不可見,但尺寸正常」——這正好是 visibility: hidden 的行為。

第一版解法:invisible absolute

單純用 visibility: hidden 有一個問題:元素雖然不可見,但仍然佔據空間,會把頁面撐開。

解法是搭配 position: absolute,讓元素脫離 Normal Flow:

vue
<ChartPanel
  ref="chartPanelRef"
  :data="data"
  class="p-12"
  :class="activeTab !== 'package_analysis' ? 'invisible absolute' : ''"
/>

<DataPanel
  ref="dataPanelRef"
  :data="data"
  class="p-12"
  :class="activeTab !== 'content_analysis' ? 'invisible absolute' : ''"
/>

這裡的 invisibleabsolute 是 UnoCSS(或 Tailwind CSS)的 utility class:

  • invisiblevisibility: hidden(保留 Layout,不繪製)
  • absoluteposition: absolute(脫離 Normal Flow,不佔空間)

兩者組合的效果:

  • ✅ DOM 存在,Canvas 有正確尺寸
  • ✅ Chart.js toBase64Image() 能正常匯出
  • ✅ 使用者看不到隱藏的 Tab 內容
  • ✅ 不影響頁面排版(不會撐開空間)

第一版的問題:多餘的 Scroll 和空白

上線後發現 invisible absolute 雖然解決了圖表匯出的問題,但引入了新的 Layout 副作用:頁面出現了不應該存在的 y-scroll,底部也多出大片空白區域。

原因在於 position: absolute 的定位行為。當隱藏的 Tab 元素被設為 absolute 時,它會相對於最近的 positioned ancestor 定位。如果沒有明確指定 top/left 等 offset 屬性,元素會停留在它原本在 Normal Flow 中的位置(即 static position),但脫離了文件流。

問題是:這個元素雖然脫離了 Normal Flow,它的 border box 仍然會被計入外層 scroll container 的 scrollable overflow area。根據 CSS Overflow Level 3 規範,scroll container 的 scrollable overflow 包含「所有它作為 containing block 的 box 的 border box」4。也就是說,即使元素是 absolute 定位,只要它的 border box 落在 scroll container 的可滾動區域內(正方向),就會撐大 scrollable area,產生多餘的 scrollbar。

改進版:Offscreen 定位

解法是把隱藏的元素移到畫面左側的「不可達區域」(unreachable scrollable overflow region):

vue
<ChartPanel
  ref="chartPanelRef"
  :data="data"
  class="p-12"
  :class="activeTab !== 'package_analysis'
    ? 'absolute invisible -z-1 pointer-events-none left-[-9999px]'
    : ''"
/>

<DataPanel
  ref="dataPanelRef"
  :data="data"
  class="p-12"
  :class="activeTab !== 'content_analysis'
    ? 'absolute invisible -z-1 pointer-events-none left-[-9999px]'
    : ''"
/>

新增的 class 各自的作用:

  • left-[-9999px]left: -9999px:把元素移到畫面左側極遠處
  • -z-1z-index: -1:確保即使意外可見也不會遮擋內容
  • pointer-events-nonepointer-events: none:防止隱藏元素攔截滑鼠事件

為什麼負方向不會產生 Scroll?

這是 CSS 規範中一個重要但容易被忽略的設計。CSS Overflow Level 3 定義了「unreachable scrollable overflow region」的概念4:在預設的 LTR(left-to-right)書寫模式下,scroll container 的起始邊(左邊和上邊)方向的 overflow 是「不可達的」——使用者無法透過滾動到達那個區域。

瀏覽器在計算 scrollable overflow area 時,會排除落在 unreachable region 的內容。因此,left: -9999px 把元素移到左側負方向,不會撐大 scrollable area,自然也不會產生多餘的 scrollbar。

相反地,如果用 left: 9999px(正方向),就會產生水平 scrollbar,因為正方向是「可達的」scrollable overflow。

效果驗證

改進後的組合:

  • ✅ Canvas 保持正確尺寸(visibility: hidden 保留 Layout)
  • ✅ Chart.js 匯出正常
  • ✅ 不產生多餘的 scroll 或空白
  • ✅ 不攔截使用者互動
  • ✅ 不影響 z-index 堆疊

為什麼不用 sr-only

可能會想到 Tailwind/UnoCSS 的 sr-only(screen reader only)class,它也是用來「視覺隱藏但保留在 DOM 中」。但 sr-only 的實作會把元素壓成 width: 1px; height: 1px; overflow: hidden,Canvas 尺寸會變成 1×1,匯出結果一樣是壞的。

sr-only 的設計目的是讓元素「存在但面積趨近於零」,給 Screen Reader 讀取用。而我們的需求是「存在且保持原始尺寸」,剛好相反。

Trade-off:資源消耗

invisible absolute 不是沒有代價。在 Vue 中三種隱藏方式的資源消耗排序:

方式Vue 指令DOMLayout 成本記憶體佔用
條件渲染v-if❌ 不存在最低
顯示切換v-show✅ 存在無(display: none 跳過)
可見性隱藏invisible absolute✅ 存在✅ 正常計算最高

invisible absolute 多了 Layout 計算的成本,因為瀏覽器需要算出每個元素的尺寸和位置。

但實際影響取決於場景。以這個 case 來說,隱藏的是兩個 Tab 的內容,Chart.js 的 Canvas 數量是固定的十幾個,Layout 成本很低。真正吃資源的是 Chart.js Instance 本身(JS Heap),而這個不管用哪種隱藏方式,只要 Instance 活著就會佔用。

適用場景

這個 pattern 適合以下情境:

  • Tab 切換中需要保持 Canvas/Chart 的渲染狀態
  • 需要在元素不可見時匯出 Canvas 內容
  • 需要保持元素的尺寸資訊(例如動態計算 Layout)
  • 元素數量有限,不會造成顯著的 Layout 效能問題

不適合的情境:

  • 大量列表項目的顯示/隱藏(Layout 成本會累積)
  • 不需要保持元素尺寸的純顯示切換(用 v-show 即可)
  • 元素可以完全銷毀重建的場景(用 v-if 更省資源)

環境資訊

  • Chart.js: 4.4.7
  • Vue: 3.x
  • Nuxt: 3.x
  • UnoCSS(invisible / absolute / left-[-9999px] utility classes)

本文撰寫時間:2026 年 3 月,技術版本可能隨時間更新,請以官方文件為準。

Reference

  1. Built-in Directives - v-show | Vue.js
  2. base64Image() with hidden container · Issue #2933 · chartjs/Chart.js
  3. visibility - CSS | MDN
  4. CSS Overflow Module Level 3 - Scrollable Overflow | W3C 2
Copyright 2020-2026 - AzureBlue ALL RIGHTS RESERVED.