はじめに (対象読者・この記事でわかること)

この記事は、Web地図アプリケーションの開発に携わっており、特にLeafletやWebGLを利用した描画を行っているエンジニアの方々を対象としています。また、Safariブラウザでの地図表示に予期せぬ問題が発生し、その解決策を探している方にも役立つでしょう。

この記事を読むことで、Safari 14へのアップデートによって発生したWebGLとLeaflet間の互換性問題の具体的な症状、その根本的な原因、そして実際に問題を解決するための複数のアプローチと具体的なコード例を理解することができます。急なブラウザアップデートによる表示崩れに頭を悩ませていた方が、本記事を通じて問題解決の糸口を見つけ、スムーズに開発を進められるようになることを目指します。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 * JavaScriptの基本的な知識 (変数、関数、オブジェクトなど) * Leafletの基本的な使い方 (地図の初期化、レイヤーの追加など) * WebGLの概念(3Dグラフィックス描画のためのAPIであること程度の理解)

Safari 14とWebGL+Leafletの互換性問題の概要

Webアプリケーション開発において、ブラウザのアップデートは常に新たな機能をもたらす一方で、予期せぬ互換性問題を引き起こすことがあります。特に、Safari 14のアップデートは、WebGLを利用したCanvas要素の描画に大きな影響を与え、Leafletなどの地図ライブラリとWebGLを組み合わせたアプリケーションで問題が頻発するようになりました。

この問題の主な症状は、Safari 14環境下でWebGLレイヤーが正しく描画されない、あるいは部分的にしか表示されないというものです。具体的には、地図の初期表示時にWebGLで描画されるはずの要素が透明になったり、地図をパン・ズーム操作した際に描画が停止したり、最悪の場合、アプリケーション全体がクラッシュすることもありました。これは、WebGLを利用して高性能なレンダリングを行う地図アプリケーションにとって、非常に深刻な問題となります。

根本的な原因としては、Safari 14でWebGLコンテキストのライフサイクル管理、特にCanvas要素のリサイズや再利用に関する挙動が変更されたことが挙げられます。従来は問題なく動作していたCanvasのリサイズや再描画のロジックが、Safari 14では特定の状況下でWebGLコンテキストを「ロスト(喪失)」させてしまったり、期待通りに再初期化されないケースが発生するようになりました。Leafletのようなライブラリは、内部で地図の移動やズームに合わせてCanvas要素のサイズ変更や再描画を頻繁に行うため、このSafariの新しい挙動と衝突し、WebGLレイヤーの描画が不安定になったと考えられます。これはSafari独自のレンダリングエンジンであるWebKitの内部的な変更に起因するものであり、開発者が簡単に回避できるものではありませんでした。

問題の再現と具体的な解決策

このセクションでは、Safari 14でのWebGL+Leaflet問題の具体的な再現方法から、その解決策までを詳細に解説します。

問題の再現環境と確認方法

まず、問題が実際に発生しているかを確認しましょう。

  1. Safari 14の環境を用意する: Mac OS Big Sur以降のSafari 14がインストールされている環境を用意します。可能であれば、他のブラウザ(Chrome, Firefoxなど)でも同じアプリケーションが正常に動作することを確認し、Safari固有の問題であることを切り分けます。
  2. 既存のアプリケーションを起動: WebGLを利用したLeafletアプリケーション(例えば、L.WebGL プラグインやカスタムWebGLレイヤーを使用しているもの)をSafari 14で開きます。
  3. 症状の確認:
    • 地図の初期表示時に、WebGLで描画されるはずのレイヤー(例: ヒートマップ、点群データ、カスタムポリゴンなど)が表示されない、または真っ白になる。
    • 地図をドラッグしてパンしたり、ズームイン/アウト操作を行ったりすると、WebGLレイヤーの描画が途中で止まる、または消えてしまう。
    • Safariの開発者ツールを開き、コンソールタブを確認します。「WebGL context lost.」や「GL_INVALID_OPERATION」などのエラーメッセージが表示されている場合があります。特に「WebGL context lost」は、ブラウザがWebGLの描画コンテキストを予期せず破棄してしまったことを意味します。

これらの症状が確認できれば、Safari 14におけるWebGL+Leafletの互換性問題に直面している可能性が高いです。

問題の原因分析と仮説

上記の問題の原因として、以下の点が挙げられます。

  • WebGLコンテキストのライフサイクル管理の変更: Safari 14では、WebGLコンテキストが失われる条件が厳しくなった、あるいはCanvas要素のリサイズやDOM操作時にコンテキストが意図せず破棄されやすくなった可能性。
  • Canvas要素の再利用/再描画ロジックの変更: Leafletが地図の移動やズームに合わせて、基盤となるCanvas要素を動的に操作(サイズ変更、スタイル適用など)する際、Safari 14の内部的なレンダリングパイプラインと競合し、描画に問題が生じる。
  • セキュリティ強化: Safariがメモリ管理やグラフィックスリソースの使用に関して、より厳格なセキュリティポリシーを適用し始めた結果、従来の描画手法がブロックされるケースが発生。

これらの要因が複合的に作用し、WebGLレイヤーの描画に支障をきたしていると推測されます。

ハマった点やエラー解決

この問題に直面した際、多くの開発者が最初に試すのは、他のブラウザでの確認やLeafletの一般的なデバッグ手法です。しかし、Safari 14固有の問題であるため、それだけでは解決に至らないことがほとんどでした。

  • 一般的なCanvas描画エラーとの混同: 通常のCanvas 2Dコンテキストや他のブラウザでのWebGLエラーと混同し、原因特定に時間を要しました。Safariデベロッパーツールでのエラーメッセージが限定的で、具体的な原因を示唆しない場合が多かったため、特定が困難でした。
  • Leafletのズームイベントハンドラでの試行錯誤: zoomendmoveend イベントで強制的に再描画を試みても、根本的なWebGLコンテキストの喪失問題が解決せず、一時的な改善にしかならないことが多かったです。
  • バージョンアップによる誤解: Leafletや関連プラグインの最新版が必ずしもSafari 14の問題に直接対応しているとは限らず、安易なバージョンアップで解決しないケースもありました。特定のSafariバージョン固有のWebKitのバグに起因するため、ライブラリ側だけでの対応が難しい局面もありました。

解決策

いくつかの解決策が提案され、効果を上げています。

1. Leafletおよび関連プラグインのバージョンアップ

最も基本的な解決策ですが、LeafletやWebGL関連のプラグイン(例: Leaflet.GLify, Leaflet.WebGLFeaturesなど)が、このSafari 14の問題に対応するパッチをリリースしている場合があります。まずは、使用しているライブラリの最新版を確認し、可能であればアップデートを試みてください。

Bash
# Leafletのアップデート npm update leaflet # Leaflet.GLifyなど、使用しているWebGL関連プラグインも同様に npm update leaflet-glify

2. WebGLコンテキストの明示的な再初期化とCanvasの再構築

Safari 14では、Canvas要素のリサイズや非表示状態からの復帰時にWebGLコンテキストが失われやすい傾向があるため、地図のパンやズーム操作の後に、WebGLレイヤーのCanvasを強制的に再構築または再初期化するワークアラウンドが有効です。

以下は、カスタムWebGLレイヤーを持つLeafletマップで、ズームイベント発生時にCanvasを再構築する一般的なアプローチです。

Javascript
// 仮のカスタムWebGLレイヤーの例 class CustomWebGLOverlay extends L.Layer { onAdd(map) { this._map = map; this._container = L.DomUtil.create('canvas', 'leaflet-zoom-animated custom-webgl-layer'); this._map.getPanes().overlayPane.appendChild(this._container); // Canvasの初期化とWebGLコンテキストの取得 this._initCanvas(); // マップのイベントリスナーを設定 map.on('zoom', this._onZoom, this); map.on('moveend', this._onMoveEnd, this); // 移動完了時にも再描画を考慮 map.on('resize', this._onResize, this); // ウィンドウリサイズ時 this._update(); // 初回描画 } onRemove(map) { map.off('zoom', this._onZoom, this); map.off('moveend', this._onMoveEnd, this); map.off('resize', this._onResize, this); L.DomUtil.remove(this._container); this._container = null; this._gl = null; // コンテキストを解放 } _initCanvas() { if (this._container) { // 既存のCanvasがあれば削除し、新しいものを生成 if (this._gl) { this._gl = null; // 古いコンテキストを明示的に解放 } // Leafletが管理するCanvasサイズに合わせる const size = this._map.getSize(); this._container.width = size.x; this._container.height = size.y; this._container.style.width = size.x + 'px'; this._container.style.height = size.y + 'px'; // WebGLコンテキストの取得 try { this._gl = this._container.getContext('webgl', { alpha: true, premultipliedAlpha: false, preserveDrawingBuffer: true // Safariで描画バッファが失われにくいように }) || this._container.getContext('experimental-webgl'); if (!this._gl) { console.error("WebGL not supported!"); } // WebGLの初期設定(ビューポート、クリアカラーなど) this._gl.viewport(0, 0, this._gl.drawingBufferWidth, this._gl.drawingBufferHeight); this._gl.clearColor(0.0, 0.0, 0.0, 0.0); // 透明 this._gl.clear(this._gl.COLOR_BUFFER_BIT); this._gl.enable(this._gl.BLEND); this._gl.blendFunc(this._gl.SRC_ALPHA, this._gl.ONE_MINUS_SRC_ALPHA); // シェーダーやバッファなどの初期化ロジックをここに記述 this._initWebGLResources(); // カスタム関数 } catch (e) { console.error("Failed to get WebGL context:", e); this._gl = null; } } } _update() { if (!this._map || !this._gl) return; const bounds = this._map.getBounds(); const topLeft = this._map.latLngToContainerPoint(bounds.getNorthWest()); const size = this._map.getSize(); L.DomUtil.setPosition(this._container, topLeft); // WebGL描画ロジックをここに記述 this._renderWebGL(); // カスタム関数 } _onZoom() { // ズーム中に描画をクリアして、ズーム完了後に再描画する準備 if (this._gl) { this._gl.clear(this._gl.COLOR_BUFFER_BIT); } this._update(); // 位置更新 } _onMoveEnd() { // 移動が完了したらCanvasを再初期化して描画 this._initCanvas(); // **ここでCanvasとWebGLコンテキストを再初期化** this._update(); } _onResize() { // ウィンドウのリサイズ時もCanvasを再初期化 this._initCanvas(); this._update(); } // WebGLリソース(シェーダー、バッファなど)の初期化関数 _initWebGLResources() { // ここにシェーダーのコンパイル、バッファの生成などを記述 } // WebGL描画を実行する関数 _renderWebGL() { if (!this._gl) return; this._gl.clear(this._gl.COLOR_BUFFER_BIT); // 描画前にクリア // ここに頂点データのバインド、描画コマンドの実行などを記述 // 例: gl.drawArrays(gl.TRIANGLES, 0, numVertices); } } // マップにレイヤーを追加する例 // const map = L.map('map').setView([0, 0], 2); // L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); // const customLayer = new CustomWebGLOverlay(); // customLayer.addTo(map);

このコードでは、_onMoveEnd (地図の移動が終了した時) や _onResize (ウィンドウがリサイズされた時) に _initCanvas() を呼び出し、Canvas要素を再構築し、WebGLコンテキストを再取得するようにしています。これにより、Safariが既存のコンテキストを予期せず破棄してしまっても、新しいコンテキストで描画を再開できるようになります。特に、preserveDrawingBuffer: true オプションは、描画バッファが破棄されにくくするために有効な場合があります。

3. requestAnimationFrame を利用した描画ループの最適化

Safari 14では、描画のタイミングが厳密になったため、安易な再描画はパフォーマンス問題や描画の途切れを引き起こす可能性があります。requestAnimationFrame を利用して描画ループを最適化し、ブラウザの描画サイクルに合わせた更新を行うことで、より安定した描画を実現できます。

Javascript
// _renderWebGL 関数内で描画ループを管理 _renderWebGL() { if (!this._gl) return; // 前回の描画リクエストをキャンセル if (this._animationFrameId) { cancelAnimationFrame(this._animationFrameId); } this._animationFrameId = requestAnimationFrame(() => { if (!this._gl) return; this._gl.clear(this._gl.COLOR_BUFFER_BIT); // ここに実際のWebGL描画コマンドを記述 // 例: gl.drawArrays(gl.TRIANGLES, 0, numVertices); // ... this._animationFrameId = null; // 描画完了 }); } // ズームや移動イベントでは、_renderWebGL() を呼び出すだけでOK _onMoveEnd() { this._initCanvas(); // Canvas再初期化は必要 this._renderWebGL(); // 再描画を要求 }

これにより、描画はブラウザの最適なタイミングで行われ、連続するイベントによる不要な再描画を避けることができます。

4. Safari Technology Previewの確認

AppleはWebKitの最新の開発状況をSafari Technology Previewで公開しています。もし、将来のSafariバージョンでこの問題が修正される予定があるかを確認したい場合、Technology Previewで試してみるのも有効です。

これらの解決策は、単独で効果がある場合もあれば、複数組み合わせて使用することで安定性が向上する場合があります。プロジェクトの特性や使用しているWebGLレイヤーの実装によって最適な方法は異なりますので、試行錯誤が必要です。

まとめ

本記事では、Safari 14のアップデートにより発生したWebGLとLeaflet間の互換性問題 を中心に解説しました。

  • 要点1: Safari 14におけるWebGLコンテキストのライフサイクル管理の厳格化が、LeafletのWebGLレイヤー描画における主要な原因であること。症状としては、描画の停止やWebGLコンテキストロストエラーが挙げられます。
  • 要点2: 問題の解決には、Leafletおよび関連プラグインのバージョンアップに加え、WebGLコンテキストを明示的に再初期化し、Canvas要素を再構築するワークアラウンドが非常に有効であること。
  • 要点3: 具体的な解決策として、_initCanvas() でCanvasとWebGLコンテキストを再生成し、map.on('moveend')map.on('resize') でこの処理を呼び出す手法が有効であり、requestAnimationFrame による描画ループの最適化も安定性向上に寄与します。

この記事を通して、Safari 14でのWebGL+Leafletの表示問題を解決するための具体的な知見と手法 を得られたことでしょう。ブラウザのアップデートは開発者にとって常に挑戦ですが、その変化に対応する知識と技術を身につけることが重要です。

今後は、ブラウザの互換性問題に特化したデバッグツールや、より汎用的なWebGL描画戦略についても記事にする予定です。

参考資料