[Mapbox]世界地図データのレンダリングが重いので最適化する

前回、世界地図データを作成し、レンダリングしていた。

Mapboxのレイヤーとして全世界のポリゴンデータ+プロパティがあると、レンダリング時に工夫の余地が残りさまざまなことができるようになる。一方で、どうしても初期データ取得時に大データの fetch やダウンロードが必要になり、レンダリングが遅くなってしまう。特にスマホのようなネットワーク帯域が弱い場合は顕著に体験が悪くなる。

この体験を改善するためにいくつか対策を実施した。

目次

データと処理を最適化をする

最適化するためには、データ通信量を小さくしつつ、並列化でデータを取得し、コード最適化を行う必要があった。
具体的には下記の3点を行うことでスムーズな地図表示を実現した。

データ量を小さくする

本来はデータ通信量を減らすのが最も大切です。実はこれが一番ネックでした。
残念ながらデータは変更ができなかったため、今回は断念しています。しかし最も効果が高い選択肢です。

元のコードでは、fetchタイミングでGeoJSONデータを取得しに行っていた。

fetch('./polygons.json')
  .then(response => response.json())
  .then(polygonGeoJSON => {
    // Layers and events are processed after data acquisition.
  });

データを prefetch する

今回はデータ通信量を減らすことができなかったので、非同期で取得できるように prefetch を考慮しました。
これでページがすぐに必要とするリソースを指定しました。

重すぎるデータを無駄に早く取ってしまうと、他のリソースの読み込みが遅れる可能性もあるので、意図しない場合は使わないのがおすすめです。

本来はAPIか、軽量なデータにして取得したかった。苦肉の策…

<link rel="preload" href="./polygons.json" as="fetch" type="application/json" crossorigin="anonymous">

不要なループを減らして最適化する

データ取得後の不要なループを見直していきます。
やりたかったことは、世界地図JSONデータの “geoJsonData” を “country_data” に一致する国だけを抽出して地図に表示することでした。この加工したデータ “polygonGeoJSON” を、map.on(‘load’, () => {}) でレイヤーに適用することです。

これは元々の処理。
fetchでthenの中の処理で、”country_data”には抽出したい国データを入れています。そして、featuresプロパティの中で geoJsonData.features の中から一致する feature だけfilterし、mapしてpropertiesをつけていました。

                        const country_data = [
                            { "SU_A3": "XXX", "country": "country_name", "lng": "-1.0000", "lat": "12.0000", "attr": [{ "name": "country_name" }] },
                        ];

                        const polygonGeoJSON = {
                            ...geoJsonData,
                            features: geoJsonData.features
                                .filter(feature => country_data.some(country => country.SU_A3 === feature.properties.SU_A3))
                                .map(feature => {
                                    const country = country_data.find(c => c.SU_A3 === feature.properties.SU_A3);
                                    if (country) {
                                        return {
                                            ...feature,
                                            properties: {
                                                ...country
                                            }
                                        };
                                    }
                                    return feature;
                                })
                        };

しかし、冗長的なループと巨大な配列サイズの場合、処理が重い可能性がありました。

そこで、こんなふうにパフォーマンスを改善しました。

  1. countryDataMap の作成:
    • country_data.reduce(…) は、country_data 配列を一度だけループします。
    • これにより、SU_A3 をキーとし、対応する国情報オブジェクトを値とするマップ(JavaScriptオブジェクト)を作成します。
  2. processedFeatures の作成:
    • geoJsonData.features.reduce() は、geoJsonData.features 配列を一度だけループします。
    • 各 feature の中で、countryDataMap[countryKey] によって国情報を取得します。オブジェクトのキーによるアクセスは非常に高速です。

reduce メソッド一つで、countryInfo が存在する場合のみ新しい feature を acc に push するため、実質的に filter と map の両方の役割を一度のループで実現しています。

特に country_data や geoJsonData.features の配列サイズが大きい場合、その差は顕著になります。前処理によって検索を効率化することで、全体の計算量を大幅に削減しています。

ループの観点では、以前は実質的にネストしたループ構造に近い処理を行っているのに対し、リファクタすることで独立した2つのループ(前処理とメイン処理)で構成されており、各ループ内での処理が軽量になります。

                        const country_data = [
                            { "SU_A3": "XXX", "country": "country_name", "lng": "-1.0000", "lat": "12.0000", "attr": [{ "name": "country_name" }] },
                        ];

                        const countryDataMap = country_data.reduce((acc, country) => {
                            acc[country.SU_A3] = country;
                            return acc;
                        }, {});

                        const processedFeatures = geoJsonData.features.reduce((acc, feature) => {
                            const countryKey = feature.properties.SU_A3;
                            const countryInfo = countryDataMap[countryKey];

                            if (countryInfo) {
                                acc.push({
                                    ...feature,
                                    properties: {
                                        // ...feature.properties,
                                        ...countryInfo
                                    }
                                });
                            }
                            return acc;
                        }, []);

                        const polygonGeoJSON = {
                            ...geoJsonData,
                            features: processedFeatures
                        };

一方で、複雑なロジックや多くの条件が絡む場合は、可読性が下がるので、処理速度に応じて map + filter を使う方も検討した方が良さそうです。

Summary

世界地図データはそのまま使うと通信量が大きくなるので、コード側で最適化する必要があります。
データを最適化できるならまずはそちらの検討から入るのがおすすめです。通信量の改善が最も効果があるからです。

シェア!

この記事を書いた人

kenichiのアバター kenichi エンジニア・写真家 | Engineer and photographer

日本全国と海外を旅するノマドワーカー。5年間、技術営業として働いたのち独立。
フリーランスエンジニアとしてWebサイト制作やアプリケーション開発を行う。面白い人たちの面白いを世に届けるべく行動中。
2024年11月にポルトガルへ移住🇵🇹

目次