現代のWeb技術に対応したキャプチャツールの「snapDOM」を試す
概要
Webページの画像キャプチャで昔 html2canvas などを使って色々ハマった経験があったのですが、最近 snapDOM というキャプチャツールがGithubのトレンドに上がっていたので、どれくらい進化しているのか試してみました。
snapDOMとは?
SnapDOMは、擬似要素、Shadow DOM、web fontsなどをサポートし、非常に高速かつ正確にHTML要素を画像にキャプチャします。
snapDOMは、ズームベースのビュー遷移フレームワークであるZumlyのために作られた、高速で正確なDOM-to-imageキャプチャツールです。
Zooming user interface 用のフレームワークの Zumly 用に作られたとの事ですが、html2canvas の時よりも擬似要素、Shadow DOMなどの進化に対応しつつ、高速という事で早速試していきたいと思います。
Benchmark
デモサイトの方にhtml2canvasとのBenchmark比較があるので試してみるとsnapDOMの方が 16.2倍早い との結果でした。

また、他のパッケージ html-to-image や古いsnapDOMバージョンとの比較もREADMEに載っています。
プロジェクト作成
今回は StackBlitz のシンプルな HTML/CSS/JS テンプレートを使って試してみたいと思います。

プロジェクトを作成し、以下の3ファイルの構成にしました。
-
index.html
<!DOCTYPE html> <html lang="en"> <head> <title>snapDOM Example</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <link rel="stylesheet" href="styles.css" /> <script type="module" src="script.js"></script> </head> <body> <main> <h1>snapDOM Example</h1> </main> </body> </html> -
script.js
import { snapdom } from 'https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs'; console.log('Hello'); -
styles.css
* { box-sizing: border-box; } body { margin: 0; font-family: system-ui, sans-serif; color: black; background-color: white; } main { padding: 1rem; } h1 { font-weight: bold; font-size: 1.5rem; }
script.js には CDN (stable) の ES Module build 形式のsnapDOMを読み込むようにしました。
簡単なサンプルを試す
試しに index.html の <div id="target"> 要素をキャプチャして、bodyの一番下に png 画像として追加するようなサンプルを試してみます。
-
index.html
... <body> <main> <div id="target"> <h1>snapDOM Example</h1> <p>12345678901234567890</p> </div> </main> </body> ... -
styles.css
#target { width: 300px; height: auto; padding: 16px; overflow: hidden; border-radius: 8px; border: thick double #32a1ce; }
この時点での表示は以下の様になっています。

次に script.js に以下を追加します。
const el = document.querySelector('#target');
const png = await snapdom.toPng(el);
document.body.appendChild(png);
再ロードすると👇の様にキャプチャされたpng画像が追加されています。ちゃんと動いてそうです ✨

Shadow DOM を試す
そもそも Shadow DOM とは…?
Shadow DOM とは、Web コンポーネントを実現するための仕組みの一つで、HTML 要素の中に“影の DOM ツリー”を作る技術です。
これにより、コンポーネント内部の構造やスタイルを外部から直接干渉されないようにカプセル化できます。たとえば、Web ページ全体の CSS がボタンのデザインを変更しても、Shadow DOM 内にあるボタンのスタイルは影響を受けません。
つまり Shadow DOM は「部品を独立して作り、他のコードやスタイルに左右されずに安全に再利用できるようにする」ための仕組みであり、モダンなフロントエンド開発において欠かせない基盤になっています。
先ほどの <div id="target"> 要素内に Shadow DOM を追加してキャプチャしてみます。
-
index.html
... <body> <main> <h1>snapDOM Example</h1> <div id="target"> <template shadowrootmode="open"> <h1>snapDOM Example</h1> </template> </div> </main> </body> ...<template shadowrootmode="open">を使って<h1>snapDOM Example</h1>を Shadow DOMとして<div id="target">内に追加しています。また通常のDOMのh1も先頭で表示させて、Shadow DOMとの違いが分かるようにしています。 -
styles.css
h1 { font-weight: bold; font-size: 1.5rem; color: red; }
この時点で👇の様に表示されます。

styles.css からの干渉を受けずに Shadow DOM内の h1 が表示されているかと思います。
これをsnapDOMでキャプチャしたものが👇になります。

ちゃんとShadow DOM がキャプチャされているのが分かります ✨
font埋め込みを試す
snapDOMは embedFonts を true に設定すると、キャプチャされたサブツリー内で使用されている@font-faceルールを埋め込んでくれるという事なので、Google Fonts を使用して試してみたいと思います。
今回は Bitcount Single Ink というフォントで試してみます。
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 以下を追加 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Bitcount+Single+Ink:wght@100..900&display=swap"
rel="stylesheet"
/>
- styles.css
#target {
font-family: 'Bitcount Single Ink'; /* 新規追加 */
width: 300px;
/* ... */
}
この時点では以下の様に表示されます。

embedFonts を指定していない為fontはキャプチャ部分には反映されてません。embedFonts に true を設定してみます。
- script.js
import { snapdom } from 'https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs';
const el = document.querySelector('#target');
const png = await snapdom.toPng(el, { embedFonts: true }); // オプション追加
document.body.appendChild(png);
👇キャプチャ部分にもちゃんとfontが反映されています ✨

ここまで実装はこちら👇になります。
Reactプロジェクトで使ってみる
次に React のプロジェクトでsnapDOMを使ってみます。StackBlitz の React (TypeScript) テンプレートを使ってプロジェクトを作成します。

プロジェクトを作ったら yarn add @zumer/snapdom でsnapDOMパッケージを追加します。
src/App.tsx に以下を追加します。
import { snapdom } from '@zumer/snapdom'; // 追加
function App() {
const ref = useRef<HTMLDivElement>(null); // 追加
// ....
return (
<div ref={ref}>
{/* ... */}
{/* 以下ボタン追加 */}
<button
onClick={async () => {
if (!ref.current) return;
const result = await snapdom(ref.current);
const img = await result.toWebp();
document.body.appendChild(img);
}}
>
Capture
</button>
</div>
);
}
この状態で「Capture」ボタンを押すと👇の様にキャプチャができました ✨

まとめ
昔の体験だと、必ず上手くキャプチャできなくて試行錯誤したという感じだったと思うのですが、今回簡単なものを試したという事もあるかもですが、snapDOM を使って上手くキャプチャできない〜みたいな事はなかった様に思いました。
参考URL
Discussion