markdown
はじめに (対象読者・この記事でわかること)
この記事は、フロントエンド開発に携わるエンジニアや、Webページ上でアニメーションを実装したいと考えている方を対象にしています。特に、複数の <div> 要素を同じスピードで順次位置変更した際に、予期せぬ隙間ができてしまう現象に悩んでいる方に最適です。この記事を読み終えると、隙間が発生する根本的な理由と、requestAnimationFrame と CSS の transition・transform を組み合わせた滑らかな実装パターンが理解でき、実際に自分のプロジェクトにすぐ適用できるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- HTML と CSS の基本的な構造・プロパティ
- JavaScript(ES6+)の変数宣言、関数、addEventListener の使い方
複数要素を同時に動かすときに起きやすい問題と基本的な対策
Webページ上で 「同じ速度で順次位置を変える」 という操作は、見た目を統一感のあるアニメーションにするためにしばしば利用されます。しかし、単純に setInterval で left や top プロパティを書き換えるだけだと、要素間に 不均一な隙間 が生じることがあります。主な原因は次の三点です。
-
ブラウザの再描画タイミングの差
setIntervalは一定の時間間隔でコールバックを呼び出しますが、実際の描画はブラウザのリフレッシュレート(多くは 60Hz)に合わせて行われます。CPU が忙しいとsetIntervalの呼び出しが遅れ、特定の<div>が他の要素よりも遅れて描画され、結果として隙間ができやすくなります。 -
レイアウト計算によるオーバーヘッド
left/topを変更するとレイアウト(reflow)とペイントが走ります。要素数が増えると計算コストが上がり、フレームごとの処理時間が不均一になるため、途中で位置がずれるケースがあります。 -
数値の累積誤差
ピクセル単位で位置を少しずつ増減させる際、浮動小数点演算の丸め誤差が累積し、予定した座標と実際の座標が微妙にずれることがあります。特にparseIntやMath.floorで切り捨てた場合、予期せぬギャップが発生しやすいです。
基本的な解決アプローチ
-
requestAnimationFrameの利用
ブラウザの描画サイクルに最適化されたコールバックでアニメーションを行うことで、タイミングのずれを最小化できます。CPU の過負荷時でも自動的にフレームレートを落とし、スムーズさを保ちます。 -
transform: translateX/Yの活用
left/topの変更はレイアウトを再計算しますが、transformはコンポジットレイヤー上だけで処理されるため、再描画コストが大幅に下がります。GPU がオフロードできる点も高速化に寄与します。 -
全要素の位置を同時更新
位置情報を配列に保持し、1 フレームごとに全要素の座標を一括で更新することで、フレーム間のずれを防ぎます。
実装例:requestAnimationFrame と transform で隙間なしアニメーション
以下では、5 つの <div> を横方向に同一速度で左から右へ移動させ、途中で隙間ができないように実装する手順を解説します。コードはモジュール化し、再利用可能な関数としてまとめています。
ステップ1:HTML と基本スタイルの用意
Html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>隙間なしアニメーションデモ</title> <style> body { margin: 0; overflow-x: hidden; } .track { position: relative; height: 120px; background: #f0f0f0; } .box { position: absolute; width: 100px; height: 100px; background: #4caf50; border-radius: 8px; /* 初期位置は左端 */ transform: translateX(0); } </style> </head> <body> <div class="track" id="track"> <!-- JavaScript で生成 --> </div> <script src="animation.js"></script> </body> </html>
.trackは全体のコンテナで、overflow-x: hiddenにすることで要素が画面外に出てもスクロールバーが出ません。.boxは動かす対象の<div>。transformだけで位置を制御するため、left/topは固定です。
ステップ2:JavaScript で要素とアニメーションロジックを作成
Js// animation.js const track = document.getElementById('track'); const BOX_COUNT = 5; const BOX_WIDTH = 100; const GAP = 20; // 各ボックス間の理想的な隙間 const SPEED_PX_PER_SEC = 150; // 1 秒あたりの移動距離 // 初期配置情報(左端に等間隔で並べる) const boxes = []; for (let i = 0; i < BOX_COUNT; i++) { const el = document.createElement('div'); el.className = 'box'; track.appendChild(el); boxes.push({ el, // 初期オフセットは i 番目の要素が前の要素の幅+隙間分だけ後ろにある位置 offset: i * (BOX_WIDTH + GAP) }); } // アニメーション用のタイムスタンプ保持 let lastTimestamp = null; function animate(timestamp) { if (!lastTimestamp) lastTimestamp = timestamp; const delta = (timestamp - lastTimestamp) / 1000; // 秒単位に変換 lastTimestamp = timestamp; // すべての box の位置を同時に更新 boxes.forEach(box => { box.offset += SPEED_PX_PER_SEC * delta; // 速度分だけ前進 // 画面右端を超えたら左端に巻き戻す(無限ループ) const totalLength = BOX_COUNT * (BOX_WIDTH + GAP); if (box.offset >= totalLength) { box.offset -= totalLength; } // CSS の transform に適用 box.el.style.transform = `translateX(${box.offset}px)`; }); // 次フレームへ requestAnimationFrame(animate); } // アニメーション開始 requestAnimationFrame(animate);
コード解説
-
配置情報の配列化
boxes配列に各要素と現在のオフセット位置を保持します。offsetはtransform: translateXに直接反映され、レイアウト計算を発生させません。 -
フレームごとの時間差 (
delta) を利用
requestAnimationFrameが渡すtimestampを使い、前回フレームからの経過時間を正確に測ります。フレームレートが変動しても、ピクセル/秒 の速度が一定になるようにしています。 -
全要素を同時に更新
ループ内部で一括更新することで、個別にrequestAnimationFrameを呼び出すよりも 同期性が保たれ、隙間ができにくくなります。 -
ループ処理
すべての要素が右端に達したら、全体幅totalLengthを引いて左端に戻すことで、シームレスな無限スクロール が実現できます。
ステップ3:ハマりやすいポイントと対策
| 項目 | 典型的な問題 | 解決策 |
|---|---|---|
setInterval によるアニメーション |
フレームごとに時間がずれ、隙間が拡大 | requestAnimationFrame に置き換える |
left/top の変更 |
レイアウト再計算が頻繁に走り、CPU が逼迫 | transform: translateX/Y に統一 |
| 小数点誤差 | 累積で 1px 以上のずれが出る | offset を Number で保持し、描画時は toFixed(2) などで丸める |
| 画面サイズ変更 | 画面幅が変わるとループ長がずれる | resize イベントで totalLength を再計算 |
実際にハマった例
問題:
BOX_COUNTを 10 に増やしたら、途中で2つのボックスが重なり、隙間が消えた
原因:totalLengthの計算がBOX_COUNTの増減に追従していなかった(ハードコーディングしていた)
対策:totalLengthを関数calcTotalLength()でリアルタイムに算出し、resizeとDOMContentLoadedの両方で再設定した
このように、全体幅 を動的に管理することが、隙間防止の鍵です。
ステップ4:パフォーマンス測定とチューニング
- Chrome DevTools の Performance タブで
Framesを確認。60fps が維持できているかチェック。 will-change: transform;を.boxに付与すると、ブラウザが事前にレイヤーを作成し、GPU アクセラレーションが有効になることがあります(ただし過度な使用は逆効果)。translate3d(0,0,0)でも同様に GPU オフロードを促すことができますが、現代ブラウザではtranslateだけでも最適化が自動的に行われます。
Css.box { will-change: transform; }
まとめ
本記事では、複数の <div> を同一速度で順次 position を変更すると隙間ができる原因を、ブラウザの描画タイミング、レイアウトコスト、数値誤差の三観点から解析しました。その上で、requestAnimationFrame と transform を組み合わせた実装を具体例とともに提示し、ハマりやすいポイントと最適化テクニックも紹介しています。
- 原因: 描画タイミングのずれ、レイアウト再計算、浮動小数点誤差
- 解決策:
requestAnimationFrame、transform: translateX/Y、全要素の同時更新 - ベストプラクティス: GPU オフロード (
will-change)、動的幅計算、フレームごとの時間差利用
この記事を読むことで、読者は 滑らかで隙間のないアニメーション を自分のプロジェクトに組み込む自信がつくでしょう。次回は、スクロール連動型のアニメーションや GSAP など外部ライブラリとの比較 について掘り下げる予定です。
参考資料
- MDN Web Docs – requestAnimationFrame
- MDN Web Docs – transform
- CSS Tricks – Using CSS Transforms for Hardware Acceleration
- 「JavaScript パフォーマンス最適化」 (O'Reilly, 2022)