ANTIGRAVITY LABEN
記事一覧/Editor View
Editor View/2026-08-29初級

Generative UI で描いたグラフは、外部参照を残したまま渡すと欠けます

Antigravity 2.11.0 の Generative UI が描く HTML は、多くの場合 CDN 参照付きで生成されます。外部参照を数える20行のチェッカーと、一枚に畳む手順を実測値つきで紹介します。

Generative UIChart.jsHTMLAntigravity 2.11

Antigravity 2.11.0(8月26日)で、HTML アーティファクトがチャットの中にそのまま描かれるようになりました。KaTeX の数式も、Chart.js や Plotly のチャートも、プレビューの中で動きます。

私が最初に書かせたのは、個人開発で続けている壁紙アプリの、カテゴリごとの枚数を棒グラフにするだけの短い一枚です。数千枚のアセットをカテゴリに振り分けていると、「いま nature が何枚で、abstract が何枚か」を目で確かめたくなる場面が定期的にあります。表よりグラフのほうが、偏りが一瞬で分かります。

出来上がったファイルは 499 バイトでした。

グラフを描いている本体は、その中に入っていません。

最小の一枚は 499 バイトで書けてしまいます

まず、実際に書いた一枚をそのまま載せます。

<!doctype html>
<meta charset="utf-8">
<title>Category counts</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<canvas id="c" width="640" height="320"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js"></script>
<script>
new Chart(document.getElementById('c'), {
  type: 'bar',
  data: {
    labels: ['nature', 'city', 'abstract', 'animal'],
    datasets: [{ label: 'assets', data: [1840, 1220, 980, 640] }]
  }
});
</script>

やっていることは、キャンバスを1つ置いて、Chart.js に棒グラフを描かせるだけです。これで十分にきれいなグラフが出ます。

ただ、このファイルが 499 バイトで済んでいる理由は、軽く書けたからではありません。描画に必要なコードが、このファイルの外にあるからです。<script src="..."> の行は「ここに 200KB 弱のライブラリを持ってきてください」という依頼であって、ライブラリそのものではありません。

チャットのプレビューの中で見ている限り、この区別は表に出てきません。困るのは、その一枚をどこかへ持ち出したときです。

外部参照が残っているかは、20行で確かめられます

目視で https:// を探すのは、ファイルが長くなるほど当てになりません。@import や CSS の url() の中に紛れていることもあります。それで、外部参照を数えるだけの小さなスクリプトを手元に置いています。

// selfcheck.mjs — HTML の外部参照を数える
import { readFileSync } from 'node:fs';
 
const html = readFileSync(process.argv[2], 'utf8');
const pats = [
  /<script[^>]+src=["']([^"']+)["']/gi,
  /<link[^>]+href=["']([^"']+)["']/gi,
  /<img[^>]+src=["']([^"']+)["']/gi,
  /@import\s+(?:url\()?["']([^"']+)["']/gi,
  /url\(\s*["']?(https?:\/\/[^"')]+)["']?\s*\)/gi,
];
 
const refs = new Set();
for (const p of pats) for (const m of html.matchAll(p)) refs.add(m[1]);
const remote = [...refs].filter((u) => /^(https?:)?\/\//.test(u));
 
console.log(`file=${process.argv[2]} bytes=${Buffer.byteLength(html)} refs=${refs.size} remote=${remote.length}`);
for (const u of remote) console.log('  REMOTE ' + u);
 
process.exit(remote.length ? 1 : 0);

先ほどの一枚にかけると、こう出ます。

$ node selfcheck.mjs cdn.html
file=cdn.html bytes=499 refs=2 remote=2
  REMOTE https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js
  REMOTE https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css

参照が2件。終了コードは 1 です。

終了コードを返すようにしてあるのは、あとで自動化に載せられるようにするためです。生成した HTML を成果物として保存する工程があるなら、その手前に一度通すだけで「持ち出せない一枚」を保存前に止められます。エージェントの変更を手元で検証する流れ自体は、Antigravity 2.10.0 の埋め込みターミナルで、エージェントの変更を検証するループを組むでも触れました。今回のチェッカーは、そのループに1行足すだけの部品として使えます。

正規表現で拾っているだけなので、動的に URL を組み立てて document.createElement('script') で差し込むような書き方は捕まえられません。そこまで凝った HTML を生成させることは私の用途ではまずないため、割り切って使っています。

参照が引けない環境では、キャンバスだけが残ります

外部参照が引けないとどうなるかを、名前解決を落とした状態で確かめました。

$ curl -sS -o /dev/null --connect-timeout 5 \
    --resolve cdn.jsdelivr.net:443:127.0.0.1 \
    https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js
curl: (7) Failed to connect to cdn.jsdelivr.net port 443 after 0 ms: Connection refused

接続は 0.0001 秒で拒否されました。タイムアウトを待つことすらありません。

ブラウザで同じことが起きると、<script> が読み込まれないまま次の行に進みます。new Chart(...)Chart is not defined で止まり、<canvas> は幅 640・高さ 320 の空白として残ります。

厄介なのは、ページそのものは壊れていない点です。タイトルは出ますし、レイアウトも崩れません。受け取った側から見えるのは「グラフが入るはずの場所が白い」という状態だけで、原因を推測する手がかりが画面上にほとんど残りません。

開く場所外部参照あり(499 バイト)外部参照なし(201,146 バイト)
チャットのプレビュー描画される描画される
ネットワークのあるブラウザ描画される描画される
機内・オフライン空白のまま描画される
外部 CDN を塞いだ社内ネットワーク空白のまま描画される
数か月後に開き直したときCDN のバージョン次第そのとき保存した見た目

最後の行が、私にとっては一番重い項目です。集計結果をグラフにして残す目的は、あとから見返すことにあります。半年後に開いて空白だったなら、残していなかったのと変わりません。

私自身、App Store に出しているアプリのアセット構成は年単位で少しずつ変わっていきます。半年前のグラフと今日のグラフを並べて初めて見えてくる偏りがあるので、古いほうが開けなくなるのは実際に困ります。

一枚に畳むのは、置換1回で済みます

やることは、<script src> の行をライブラリの中身に差し替えるだけです。

// inline.mjs — CDN 参照をファイルの中身に差し替える
import { readFileSync, writeFileSync } from 'node:fs';
 
let html = readFileSync('cdn.html', 'utf8');
const js = readFileSync('chart.umd.min.js', 'utf8');
 
html = html.replace(
  /<script src="https:\/\/cdn\.jsdelivr\.net[^"]*"><\/script>/,
  '<script>' + js + '</script>'
);
html = html.replace(/<link rel="stylesheet" href="https:\/\/cdn\.jsdelivr\.net[^"]*">\n/, '');
 
writeFileSync('inline.html', html);

数式を使っていないので、KaTeX の CSS はここで落としています。使う分だけを残すほうが、結果的に小さくなります。

畳んだあとにチェッカーをもう一度通すと、こうなります。

$ node selfcheck.mjs inline.html
file=inline.html bytes=201146 refs=0 remote=0

外部参照 0 件、終了コード 0 です。ファイルは 499 バイトから 201,146 バイトになりました。増えた分のほとんどは Chart.js の UMD ミニファイ版で、これが 200,807 バイトです。

なお、速度が理由で畳んでいるわけではありません。同じ CDN からの取得を3回計ったところ、0.107 秒・0.117 秒・0.110 秒でした。まっとうな回線があれば、この往復は体感に出ません。

畳む理由は、開ける場所が増えることのほうです。200KB のファイルは、メールに添付しても、リポジトリに置いても、USB メモリに入れても、そのまま開きます。

畳まないほうがよい場面もあります

とはいえ、毎回畳んでいるわけではありません。私は次のように分けています。

状況私の選択理由
チャットの中で一度見て終わりそのままプレビューでは差が出ない。畳む手間のほうが大きい
ライブラリや設定を試している最中そのまま差し替えるたびに畳み直すのは無駄
誰かに渡す・共有する畳む相手の回線とネットワーク制限が分からない
リポジトリや記録として残す畳む数か月後の再現性がライブラリ側の都合に左右されない
移動中や機内で見返す予定がある畳むオフラインで確実に開く

境目は「この一枚は使い捨てか、残すか」です。使い捨てなら 499 バイトのまま捨ててしまってよく、残すなら畳む。判断の材料はそれだけで足ります。

Generative UI が入ってから、集計をその場でグラフにしてもらう回数が明らかに増えました。手軽に出せるぶん、そのうちの何枚かは残したくなります。残す側に回った一枚だけ、チェッカーを一度通す。それくらいの手数で足りています。

まずは、最後に描かせた HTML を1つ選んで node selfcheck.mjs にかけてみてください。remote=0 で返ってくるか、それとも数字が並ぶか。そこが分かれば、畳むかどうかの判断はすぐに付きます。

最後までお読みいただき、ありがとうございました。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Editor View2026-08-27
Antigravity 2.10.0 の埋め込みターミナルで、エージェントの変更を検証するループを組む
2.10.0 でサイドバーから直接ターミナルを開けるようになりました。エージェントの変更を通す前の検証を3つのコマンドに固定し、変更ファイルだけを対象にする手順を、実測値とスクリプト付きでまとめます。
Editor View2026-08-27
日本語を含むファイルをエージェントに編集させる前に、バイト列で確かめている3つの検査
エージェントの編集で日本語が1文字だけ入れ替わっても、git の差分は普通の1行変更として通ります。不正な UTF-8・置換文字・正規化ゆれの3つを、1パスの検査にまとめるまでの運用記録です。
Editor View2026-08-23
WebP と Opus の添付が通らないときの切り分け順序
8月20日のハブ 2.9.1 で WebP の添付に対応し、同じ日の CLI 1.1.17 では Ogg 系の音声がモデルに拒否される問題が直りました。拒否がファイル側ではなく送信側の MIME 判定で起きる仕組みを、手元で実際に測った判定結果とともに切り分けます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →