この記事は韓国語から翻訳したものです。不十分な部分があれば、いつでもフィードバックをいただければありがたいです! (オリジナル記事, 同じく私が作成しました。)
バスハニャーンサービスの開発を総括しながらUXを改善するための様々な努力を書いてみたいと思います。
Speed
ウェブサービスでスピードは命だと思います。Googleの研究結果によると、モバイルウェブページの読み込み速度が増加するにつれて、ユーザーの離脱率がものすごく増加することが分かる。バスハニャーンサービスは、漢陽大学ERICAキャンパスのシャトルバスの時刻表を見ることができる簡単なウェブサービスで、ユーザーに素早く簡単に提供されなければならない。
バスハニャーンの接続速度を向上させるために、次のような最適化を行いました。
Service Worker Caching
バスハニャーンはウェブサービスですが、アプリのメリットも活かすためにPWAのインストールをサポートします。PWAで最も核心的な部分であるService Workerの代表的な機能の一つがキャッシングです。サーバーから提供されるファイルをローカルにキャッシュしておくことで、読み込み速度を大幅に改善してくれる役割をします。 また、PWAをインストールしなければキャッシュができないわけではないので、サービスを何度も利用するユーザーの立場では、データ節約や速度向上が期待できます。
キャッシュをした場合のデメリットももちろん存在します。キャッシュされたファイルにより、サービスアップデートバージョンの配布後、ユーザーにアップデートがすぐに反映されない場合があります。このような場合は、Service Worker側でキャッシュが自動的に更新されるまで待つ必要があります。
バスハニャーンサービスは Vite
を利用して作られています。Viteには
vite-plugin-pwa` というプラグインがあり、Service Workerとmanifestファイルを自動で生成してくれます。
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
// Service worker setting
injectRegister: 'auto', // Service worker mode. This decides the injection method.
registerType: 'autoUpdate', // New version update mode.
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], // File regex to cache
},
}),
],
})
vite.config.ts
で以下のように設定すれば完成です。Defaultとほぼ同じです。しかし、フォントファイルやアイコンリソースもキャッシュしたいので、拡張子パターンを追加しました。
サービスへアクセスしてみると、Sizeタブで容量が測定されず、(ServiceWorker)
と表示され、正常にローカルにキャッシュされてることが確認できます。また、画像の下部にある転送量を見ると、実際に転送された容量がリソースの容量に比べて圧倒的に少ないことが確認できます。
Dynamic Subset Font
バスハニャーンサービスはPretendard
というオープンソースライセンスのフォントを使用しています。このフォントを使う場合、ウェブ上でwoff2という圧縮されたフォント形式で提供されるのですが、そのフォント容量が2.2MBです。もちろん、インターネットが速い世の中なので2.2MBはすぐに読み込みますが、インターネットがもしかしたら遅い場合には、フォントが遅く読み込まれ、フォント適用前の文字が画面にレンダリングされた後、フォントの読み込みが終わると、フォントが変更される不思議な現象が発生することがあります。(参考)もちろん、Service Workerを使うのでキャッシュして次回アクセスする時は大きな問題はないが、基本的に私たちのサービスをアクセスするためのページリソース容量自体を減らしたいと思った。
Pretendard自体でDynamic subsetを提供していたので、適用は難しくなかったです。
@import url("https://cdnjs.cloudflare.com/ajax/libs/pretendard/1.3.5/static/pretendard-dynamic-subset.css");
一般フォントの代わりにsubsetフォントを読み込めば終わりです。Dynamic Subset Fontが適用されると、ページをロードする時、次のようにフォントファイルが分割されてロードされることが確認できます。その結果、初期に読み込むフォントの容量をほぼ2MB近く減らすことができました。
Partytime
バスハニャーンサービスで適用した技術の中で一番新しい技術です。このライブラリはウェブにあるサードパーティスクリプトをメインスレッドでロードさせず、Service Worker内部で実行するようにします。重いサードパーティースクリプトがある場合、メインページをローディングするスレッドから離れて別々にローディングをするため、より快適にウェブサイトをローディングすることができます。(参考)
代表的なサードパーティースクリプトとしてはグーグルアナリティクスとフェイスブックピクセルがあります。バスハニャーンサービスはグーグルアナリティクスを使うので、そのスクリプトをウェブページヘッダーでロードさせず、Partytimeを利用して実行することにしました。
公式ホームページのガイドラインを読んでみると、各フロントエンドライブラリごとにどのように適用する必要があるのかが書いてあります。バスハニャーンサービスはReactを使うので、次のように適用しました。
1.パッケージ追加
yarn add @builder.io/partytown
2.React側のメインコンポーネントにPartytownコンポーネントを追加(例)
import { Partytown } from '@builder.io/partytown/react';
export function Head() {
return (
<>
<Partytown debug={false} forward={['dataLayer.push']} /> // dataLayer.push -> GTAG only
</>
);
}
3.スクリプトをPartytownに入れます。
<script src="https://www.googletagmanager.com/gtag/js?id=YOUR_GTAG_HERE" type="text/partytown"></script>
<script type="text/partytown">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'YOUR_GTAG_HERE');
</script>
Script typeが標準規格ではないので、IDEでもlintingされません。でも心配しないでください。
4.ビルド時に特定のディレクトリにPartytownのビルドされた成果物を移動するように設定する
import { partytownVite } from '@builder.io/partytown/utils'
import path from 'path'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
partytownVite({
dest: path.join(__dirname, 'dist', '~partytown'),
}),
],
publicDir: './public',
})
ここまでやったら、適用は終わりです。
もちろん、このライブラリのメリットはそれだけではありません。開発者ツールのネットワークタブを見ると、無数の proxytown
リクエストが表示されます。これはPartytownとService Workerの間で通信するリクエストなので、ローカルで通信しているため、実際のパフォーマンスには影響しません。(参考)
Lazy Loading
バスハニャーンにはメインページと時刻表ページの2つのページしかありません。しかし、メインページをロードする時、全体ページも一緒にロードする必要があるのでしょうか?残念ながら、Routeの中に全体時刻表コンポーネントを入れると、メインページがロードされる時、Routeの中にある全体時刻表コンポーネントも一緒にロードされます。このように不要なコードを一度にロードすることを減らすためReact.lazyとSuspenseを使ってコードを分割することができます。
既存のコードを見てみると、次のような感じです。
<Route path="/all" element={<FullTime />} />
ただRouteを使うとそのコードがあるtypescriptとそのRouteの中にあるコンポーネントも一緒に入ってロードします。
import React, { lazy, Suspense } from 'react'
import { Route } from 'react-router-dom'
const FullTime = lazy(() => import('./app/components/FullTime'))
<Route
path="/all"
element={
<Suspense fallback={<div />}>
<FullTime />
</Suspense>
}
/>
既存のコードを次のように変えればコード分割が完了します。全体時刻表ページを押したらtypescriptが別々にロードされることを開発者ツールのネットワークタブで確認することができます。
0-RTT Resumption
「 https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/ 」
2008年度に出た規格であるTLS 1.2では、安全な接続のためにTCPとTLSがHandshakingする過程で必要な相互間の通信回数が多かったです。しかし、2018年8月に新しく登場したTLS 1.3では、従来のTCPの代わりにQUICという新しいプロトコルを利用して、このHandshakingプロセスを大幅に削減しました。
「 https://blog.cloudflare.com/the-road-to-quic/ 」
しかし、ここで終わりではありません。ユーザーが再接続した場合、保存したPSKをアプリケーションデータと一緒に送れば、別途の追加Handshakeが必要なく、サーバーから暗号化されたデータを再び受け取ることができます。Round trip回数がHTTPと変わらないので、これによって暗号化接続速度を向上させ、リソースの節約が可能です。
「 https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/ 」
0-RTTを使用すると、replay attack
というネットワーク攻撃に脆弱になりますが、バスハニャーンが利用するAPIでサーバーの値を直接的に変更するAPIはなく、単に読み取るAPIだけなので、安心して0-RTTを有効にしました。
Result
PageSpeed Insightsで実際のユーザーの経験を確認した結果です。コアウェブバイタル評価でかなり順守した評価を受けたことが分かります。
SEO
外部に公開されたサービスであるため、検索エンジンで検索した時もよく出てくるはずです。コアキーワードだけ登録して検索エンジンに露出させる方法はGoogleがサポートしてないので、検索エンジンによく露出されるようにSEO最適化をしました。最適化はChrome開発者ツールに内蔵されたLighthouseを参考にしました。
Lighthouse
Lighthouseは開発者ツールを開くとすぐに利用することができます。Lighthouseが案内する最適化の内容は次の通りです。
最初は該当しない項目がいくつかあったが、下に書く予定の項目を解決したら全部解決されました。
Crawling
バスハニャーンサービスはシャトルバスの時刻表を表示するサービスなので、実質的にクロールできる内容があまりありません。しかし、検索エンジンにサービスで使われたアイコンや画像が画像タブに表示されるのは嫌なので、画像は収集しないようにクローラーに伝えるrobots.txt
を作成しました。
User-agent: *
Allow: /
Disallow: /*.png$
Disallow: /*.svg$
Disallow: /*.jpg$
Disallow: /*.jpeg$
robots.txtは別にビルドする必要がなく、ビルドディレクトリrootに存在すればいいです。従って、ビルド結果物にこのファイルを含ませてくれます。
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
// Deployment
includeAssets: [
'favicon.svg',
'favicon.ico',
'robots.txt',
'safari-pinned-tab.svg',
],
}),
],
publicDir: './public',
})
Meta Tags / Opengraph
検索エンジンがサイトに関する情報を取得する部分がメタタグである。検索エンジンのクローラーはウェブページの <head>
にあるメタタグを読むことでサイトをクロールする。かなり多くのメタタグがありますが、全てのメタタグを入れずに必要だと思ったタグだけ追加しました。
検索エンジン必須タグを除いて一番代表的でよく使われるタグがOpengraphタグです。Opengraphはメタタグの種類の一つであり、SNS共有、特にFacebookに最適化されているタグである。Opengraphタグに関する詳しい情報はこちらで確認できる。
リニューアルされる前のバスハニャーンサービスのアナリティクスでreferral linkを分析してみた結果、SNSで共有される現象は非常に少なかった。 特に最近の大学生はFacebookをあまり使わない。
せめて広報を始めたEverytimeの流入が多いが、ここにはリンクのプレビューがない。 そのため、カカオのOpengraphの規格を一部使うことにした。
カカオトークのガイドラインを見ると、唯一ユニークな部分がOpengraph imageです。ここカカオdevtalkを確認してみると、Opengraphスクラップ画像の推奨サイズは800px * 800px、正方形です。この規格でOpengraph imageを入れると、カカオトークやカカオストーリーでリンクを共有した時、自動的にcropされてきれいに見えるそうです。したがって、このガイドラインに沿ってimageタグを挿入し、テストした結果、綺麗に表示されました。
適用する時、OGキャッシュを初期化しても適用されないバグがあり、大変苦労しましたが、カカオは共有されたリンクサイトのOpengraph imageタグを直接読まずにOpengraph urlタグにある値(リンク)のOpengraph imageを読み込みます。
PWA
最後にPWAです。PWAもLighthouseで分析が可能です。PWAとは何かとPWAのサポート条件については以前の記事で説明したので、この記事では省略します。
ここで先ほど追加できなかったメタタグを追加します。
Results
たとえ、インターネットやパソコン環境によって点数のばらつきがあるものの、結果的にLighthouseでとても高いスコアを記録しました。
しかし、今回は開発者の立場から見たユーザーの利便性と性能だけを考慮して修正された。まだリニューアル版が配布されたばかりなので、今後、データを収集し、ユーザーの意見を取り入れてUXをさらに改善していきたいと思います。