並び替えたときにカードが滑らかに動く表示(FLIP と呼ばれるやり方)を作り、動くかどうかを確かめていました。確かめる作業は生成AI(Claude Code)に任せていて、AIはアプリの中のブラウザでページを開き、JavaScript で要素の位置とアニメーションの状態を読んでいました。
返ってきた結果は「動いていない」でした。Web Animations API のアニメーションは pending のまま進まず、測った位置は 0。コードを何度読み直しても、おかしいところは見つかりません。
原因:ページが画面に出ていなかった
そのブラウザの画面は、測っている間ずっと閉じた(非表示の)状態でした。
ブラウザは、見えていないページの描画を止めて電気と CPU を節約します。非表示のページでは requestAnimationFrame のコールバックがほとんど呼ばれず、描画に合わせて進む処理(アニメーションの進み・ResizeObserver など)も止まります。コードが正しくても、見えていないページで測ると「動かない」と出ます。
背景のタブで同じことが起きます。人が操作していれば気づきますが、自動で測ると「ページが見えているか」は結果に出てきません。
直し方1:測る前に document.hidden を見る
まず、測った結果と一緒に「そのページが見えていたか」を出すようにします。
console.log(document.hidden, document.visibilityState); // true, "hidden" なら測っても意味がない
描画が進んでいるかは、1秒間に requestAnimationFrame が何回呼ばれたかで分かります。
let n = 0;
const tick = () => { n++; requestAnimationFrame(tick); };
requestAnimationFrame(tick);
setTimeout(() => console.log({ hidden: document.hidden, frames: n }), 1000);
// 見えているページなら { hidden: false, frames: 60 } くらい。0〜1 なら描画が止まっている
直し方2:動きの確認は headless Chrome で測る
動き・リサイズの確認は、画面の表示に左右されない headless のブラウザで測るようにしました。Playwright で手元の Google Chrome を使う例です(channel: 'chrome' にすると、Playwright 同梱のブラウザを入れていなくても動きます)。
import { chromium } from 'playwright';
import { pathToFileURL } from 'node:url';
const browser = await chromium.launch({ channel: 'chrome' }); // 手元の Google Chrome を使う
const page = await browser.newPage();
await page.goto(pathToFileURL('page.html').href);
const state = () => page.evaluate(() => {
const el = document.getElementById('box');
const a = el.getAnimations()[0];
return {
hidden: document.hidden,
playState: a?.playState,
currentTime: Math.round(a?.currentTime ?? 0),
x: Math.round(el.getBoundingClientRect().x),
};
});
console.log('開始直後', await state());
await page.waitForTimeout(500);
console.log('0.5秒後', await state());
await page.waitForTimeout(800);
console.log('1.3秒後', await state());
await browser.close();
page.html は、四角を1秒で右へ300px動かすだけのページです。
<!doctype html><meta charset="utf-8">
<div id="box" style="width:80px;height:80px;background:#36c"></div>
<script>
document.getElementById('box').animate(
[{transform:'translateX(0)'},{transform:'translateX(300px)'}],
{duration:1000,fill:'forwards'}
);
</script>
npm i -D playwright
node check-anim.mjs
# 開始直後 { hidden: false, playState: 'running', currentTime: 17, x: 13 }
# 0.5秒後 { hidden: false, playState: 'running', currentTime: 533, x: 168 }
# 1.3秒後 { hidden: false, playState: 'finished', currentTime: 1000, x: 308 }
hidden: false のまま、時間に合わせて x が進み、最後に finished になれば動いています(x の 8 は body の余白の分です)。
使い分け
- 見た目(色・レイアウト)のスクリーンショット:画面に出ているブラウザで撮る
- 動き・リサイズ・時間で変わるもの:headless で測る。結果に
document.hiddenを必ず並べる
まとめ
- 非表示のページ・背景のタブでは
requestAnimationFrameがほぼ止まり、アニメーションは進まない - 自動で測るときは、結果と一緒に
document.hiddenと1秒あたりのフレーム数を出す - 動きの確認は headless Chrome(Playwright の
channel: 'chrome')で測る
生成AI(Claude Code)に書かせて一人で会社を回している記録を note に書いています。
https://note.com/kaito_cometa