スムーズスクロールライブラリを入れた途端、iPhoneのSafariやBraveで奇妙な現象に出会いました。下方向にスクロールして指を離した瞬間、画面がさらに1段下にカクっと落ちます。上スクロールでは何も起きません。
iOS WebKitの rubber-band や慣性スクロールが、JavaScriptで制御するスクロールと競合しやすい。これが真因です。
症状: iOSでスクロール停止時にカクっと落ちる
具体的な症状は、こうです。日本など多くの地域ではiOSブラウザはWebKitベース (EU では 2026 年時点で代替ブラウザエンジンの制度が始まっていますが、日本市場では実務上ほぼWebKitと捉えてよい)。iOSのSafari、Brave、Chromeで発生します。下スクロールして指を離した直後、画面がもう1段下にカクっと動く。上スクロールでは発生しません。デスクトップでも発生しません。
「Lenisを入れた途端」「ScrollTriggerを入れた途端」「scroll-driven transformを入れた途端」など、JavaScriptがスクロールに絡む処理を始めた瞬間に、出始めます。
原因: rubber-bandとJS制御スクロールの競合
iOS WebKitは、タッチスクロールの終端で2種類の特殊な挙動を持ちます。
一つは rubber-band。スクロール可能範囲を超えてドラッグすると、コンテンツが伸びるように動き、指を離すと元に戻る。もう一つは慣性スクロール。指を離した後もモメンタムでスクロールが続き、徐々に減速して停止します。
この間、scroll イベントは連続発火します。JavaScriptで「スクロールに応じて何かを動かす」処理を書いていると、rubber-bandの戻り動作中も「スクロールが下に動いた」と認識してtransformを更新してしまう。視覚的には「スクロールが止まったあと、もう1段動いた」と知覚されます。
スムーズスクロールライブラリ(Lenis等)はさらに踏み込んでいて、transform: translateY(...) でスクロール位置をJavaScript制御します。これがiOSのネイティブスクロールと干渉して、停止瞬間に位置補正が入ってカクつきます。
対策1: ホバー機能を持たないデバイスでJSスクロールを無効化
JavaScript制御のスクロールは、タッチ操作には本来不要です。最近のモバイルOSのネイティブスクロールは、慣性、rubber-band、scroll restorationまで含めて十分に練られています。デスクトップのホイールスクロール体験を改善したいだけなら、ホバー機能を持たないデバイスではオフにします。
import Lenis from 'lenis';
let lenis: Lenis | null = null;
function initLenis() {
// prefers-reduced-motionで無効化
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
// ホバー機能を持たないデバイス (hover: none) で無効化
if (window.matchMedia('(hover: none)').matches) return;
lenis = new Lenis({ duration: 1.2, easing: ... });
// ...
}
@media (hover: none) は「ホバー機能を持たない入力デバイス」を意味し、タッチ主体のスマホやタブレットで真になります。ただし hover: none だけでは完全にタッチ専用とは言えず、ペン入力環境やハイブリッド端末で結果がぶれることがあります。より厳密に振り分けたいなら、pointer: coarse 併用や実機での挙動確認を組み合わせます。
これでモバイルではネイティブスクロール、デスクトップではLenisという分岐ができます。
対策2: scrollYのクランプ
scroll-driven transform (背景のThree.js、parallax、scrubアニメーション等) を使っている場合、scrollYを読み取るときに負値をクランプします。
let scrollY = Math.max(0, window.scrollY);
window.addEventListener('scroll', () => {
scrollY = Math.max(0, window.scrollY);
}, { passive: true });
iOSの rubber-band 中は window.scrollY が一瞬負値を取ります。クランプしないと、scroll-drivenな scale、rotation、opacity が「逆方向に動く」一瞬を生みます。
Brave単独の症状なら、深追いせずに記録する
iOSのBraveはWebKitベースで、レンダリングエンジンはSafariと同じはず。にもかかわらず、Braveだけでカクつきが継続するケースがあります。
切り分けの順序を決めておくと、無駄な時間を使いません。
Safariで再現するか。Yes ならWebKit全般の問題で、上の対策で改善します。No ならBrave固有の挙動を疑う。Brave Shieldsをオフにして再現するか。継続するならShields以外のBrave機能 (Fingerprint Protection、カスタムcontent blocker) を疑います。
Shields off でも症状が継続するなら、Brave単独問題と判定してサイト側修正は諦めます。BraveのFingerprint Protectionが performance.now() や requestAnimationFrame のタイミング精度を制限することは公開されていて、scroll-driven な精密制御に影響する可能性があります。ただし症状の再現要因は単一とは限らないので、「Brave 固有の挙動の組み合わせで起きている」程度に留めて深追いしません。
特定ブラウザの特定機能の組み合わせでのみ起きる症状を、サイト側で100%抑え込むのは割に合いません。Safari、Chrome、Firefox の主要3、iOS Safari、Android Chrome で再現しないなら、ブラウザ単独問題として記録し、利用者にはオプション解除を案内します。
iOS WebKitの rubber-band とJavaScript制御スクロールは、競合しやすい関係。スムーズスクロールライブラリは「デスクトップのホイール体験の改善ツール」と割り切り、タッチデバイスではネイティブスクロールに任せるのが現実的です。