- 投稿日:2022-02-14T23:11:47+09:00
Quasar + Vite + TypeScript で始める爆速 Vue 3 開発
はじめに この記事では、次のような構成で Vue 3 開発を行うためのセットアップ手順について紹介します。 言語: TypeScript ビルドツール: Vite UI フレームワーク: Quasar Quasar とは Vue 3 に対応した Material Design の UI フレームワークです。 「パフォーマンス重視」を謳っていることが特徴のようです。 同じく Vue 用の Material Design UI フレームワークとしては Vuetify が人気ですが、Quasar もそれに負けないくらいの機能を持っておりドキュメントも充実しています (日本語のドキュメントはありません)。 また、Vuetify に比べるとかなり早く Vue 3 に対応できている1点も魅力です。 ちなみに便宜上ここでは UI フレームワークとして紹介しましたが、実際には UI コンポーネントつきの Vue フレームワークといった方が適切で、立ち位置的には Vuetify よりも Nuxt.js の方が近いかもしれません。 そのため単なる UI フレームワークを求めている場合、Quasar では機能が過剰で要求に合わない可能性があります。 ただしセットアップ方法次第では単なる UI フレームワークのように扱うことも可能です。今回紹介するセットアップ方法がこれに当たります。 Vite とは Vue や React, Svelte などをサポートしている高速なビルドツールです。 Vite はソースコードを Native ESM として扱います。 そして開発時にはソースコード全体をバンドルするのではなく、ブラウザからのリクエストに応じて必要なモジュールのみを提供する仕組みとなっています。 これによりバンドル型のビルドツールと比較して開発サーバの起動や HMR が非常に高速となっています。 ソースコードを Native ESM で扱う都合上、Vite は 依存関係の事前バンドル を行い CommonJS や UMD を ESM に変換します。 この事前バンドルは esbuild によって行われるため非常に高速且つキャッシュもされるのですが、変換が必要な大きな依存パッケージがあるとやはりその分の時間はかかってしまいます。 その点 Quasar は ESM でも提供されているため変換が不要であり、高速な起動が期待できます。 注意 この記事では Vite で Vue プロジェクトを構築し、Quasar は Vite プラグインとして導入します。 この場合 Quasar は単なる UI フレームワークとして使用することになり、Quasar CLI による開発支援など Quasar のフル機能の恩恵を受けることはできません。 執筆時点ではこれが Quasar + Vite を実現するためのオフィシャルな方法ですが、将来的には Quasar CLI でビルドツールのオプションとして Vite を選択できるようにする計画があり、そちらが推奨されるようです。 Quasar を UI フレームワークとして使うのではなく、Quasar アプリケーションのビルドツールとして Vite を使いたい場合は公式ドキュメント等から最新の方法をチェックするようにしてください。 環境 この記事では次の環境を使用しています。 Node.js v16.14.0 npm v8.5.0 なお、以下のセットアップ手順はあくまでも執筆時のものなので、正確な手順は公式ドキュメントを参照してください。 Vue プロジェクト作成 Vite の公式ドキュメントに従って新規 Vue プロジェクトを作成します。 TypeScript を使いたいので vue-ts テンプレートを使用します。 $ npm create vite@latest my-app -- --template vue-ts プロジェクトが scaffold されたら、依存パッケージのインストールを済ませて動作確認をします。 npm run dev で開発サーバを起動することができます。 $ cd my-app $ npm run install $ npm run dev vite.config.ts で Node.js の機能を使えるようにしておきましょう。 Node.js の型定義ファイルをインストールします。 使用している Node.js のメジャーバージョンに合わせてバージョン指定しています。 $ npm install --save-dev @types/node@16 tsconfig.node.json で ES Module Interop を有効にしておきます。 これにより path などの標準モジュールを default import できるようになります。 tsconfig.node.json { "compilerOptions": { "composite": true, "module": "esnext", "moduleResolution": "node", "esModuleInterop": true }, "include": ["vite.config.ts"] } src/ 以下を @ で import できるようにする (Optional) 鉄板の設定として src/ 以下のファイルを @ で import できるようにしてみます。 これを、 src/App.vue import HelloWorld from './components/HelloWorld.vue'; こんな風に import できるようにするための設定です。 src/App.vue import HelloWorld from '@/components/HelloWorld.vue'; tsconfig.json に Base Url と Paths の設定を追加して、TypeScript が @ を src/ 以下のファイルで解決するようにします。 tsconfig.json { "compilerOptions": { "target": "esnext", "useDefineForClassFields": true, "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "esModuleInterop": true, "lib": ["esnext", "dom"], "baseUrl": "./", "paths": { "@/*": ["src/*"] } }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } これだけだとビルド時のパス解決は行われないので、Vite 側でも resolve.alias を設定して、@ を src/ 以下のファイルで解決するように設定します。 vite.config.ts import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import path from "path"; export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "src"), }, }, plugins: [vue()], }); Quasar 導入 Vite を使用する場合は次のドキュメントに記載された手順に従ってセットアップします。 quasar と @quasar/extras をインストールします。 @quasar/extras はフォントやアイコン、アニメーションなどが入ったパッケージで、必須ではありません。 個人的に Material Icons を使いたいので導入しています。 $ npm install quasar @quasar/extras @quasar/vite-plugin と sass を devDependencies に追加します。 sass も Optional ですが、Quasar で定義された Sass / SCSS 変数 を使用したい場合に必要となります。 細かいバージョン指定があるので注意してください。これは dart-sass v1.33.0 で / による除算が非推奨になった ことに由来しているものと思われますので、1.32.x の最新版を入れておけば OK です。 $ npm install -D @quasar/vite-plugin sass@1.32.13 公式ドキュメントの configurator を活用して設定ファイルを生成します。 今回は Material Icons を有効にし、日本語の Language Pack を使用して Quasar コンポーネントを日本語化する設定にしてみました。 生成された main.js の内容を main.ts にコピーします。 main.ts import { createApp } from "vue"; import { Quasar } from "quasar"; import quasarLang from "quasar/lang/ja"; import "@quasar/extras/material-icons/material-icons.css"; import "quasar/src/css/index.sass"; import App from "./App.vue"; const myApp = createApp(App); myApp.use(Quasar, { plugins: {}, lang: quasarLang, }); myApp.mount("#app"); 生成された vite.config.js の内容を vite.config.ts にコピーします。 すでに vite.config.ts をいじっている場合はいい感じにマージします。 vite.config.ts import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import { quasar, transformAssetUrls } from "@quasar/vite-plugin"; import path from "path"; export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "src"), }, }, plugins: [ vue({ template: { transformAssetUrls }, }), quasar({ sassVariables: "src/quasar-variables.sass", }), ], }); 生成されたものをコピーして src/quasar-variables.sass を作成します。 src/quasar-variables.sass $primary : #1976D2 $secondary : #26A69A $accent : #9C27B0 $dark : #1D1D1D $positive : #21BA45 $negative : #C10015 $info : #31CCEC $warning : #F2C037 VSCode の設定をする (Optional) エディタとして VSCode を設定している場合は Vue 開発用の Extension として Volar をインストールしておきましょう。 Vue 2 開発で Vetur を使用していた場合は Volar と競合するので無効化しておきます。 アプリケーションを開発する これでセットアップは完了なので実際に開発を始めることができます。 繰り返しになりますが、npm run dev で開発サーバを起動することができます。 Vite のスピードを体感しましょう。 $ npm run dev また、npm run build で本番ビルドが可能です。 $ npm run build 本番ビルドの成果物は npm run preview で起動したローカルサーバで確認することができます。 $ npm run preview Quasar の使い方についての詳しい解説はしませんが、まずは Quasar 公式ドキュメントの Layout and Grid や Vue Components のあたりを眺めつつ色々と触ってみると大体わかってくると思います。 また、公式に提供されている Layout Builder もとっかかりとしておすすめです。 サンプル 今回紹介した構成で簡単な ToDo アプリを作ってみました。 ディレクトリ構成などは Quasar CLI で生成されるものに寄せています。 こちらでは Vue Router の導入や ESLint の設定なども行っているので気になる方は参考にしてみてください。 Vuetify の Vue 3 対応バージョンである v3 (Titan) はリリースが遅れており、執筆時点では 2022 年 5 月リリース予定となっています。 ↩
- 投稿日:2022-02-14T20:56:57+09:00
[Vue3 + Swiper8] 独自デザインのナビゲーションとページネーションを実装する
はじめに 高機能カルーセルライブラリといえば自分は Swiper をおすすめします。 Vue、React、Angularなどに対応し、カスタマイズ機能が豊富。 ただしバージョン毎にクセがあって、アップデートの度に導入トラブルで苦しめられていたのですが(自分の技量が足りない)、最新版の 8.0.5 ではスムーズに導入できたので備忘録として残します。 インストール swiper をインストールするだけです。 $ yarn add swiper もし css-loader、vue-style-loader がなければインストールします。 $ yarn add --dev css-loader vue-style-loader Webpack設定 この記事では Vue での開発環境が整っている前提ですので詳細は省きます。 webpack.config.js の module.rules に CSS設定を追加します。 webpack.config.js module.exports = { // 〜略〜 module: { rules: [ // Vue設定 { test: /\.vue$/, exclude: /node_modules/, use: [{ loader: 'vue-loader', }], }, // css設定 { test: /\.css$/, use: ['vue-style-loader', 'css-loader'], } ], }, // 〜略〜 } style-loader も入れたらエラーになった 上の結論になる前に style-loader も入れていたのですが、実行時にエラーになりました。 この辺の組み合わせはいまだによくわかってません。 // 実行時にエラー use: ['vue-style-loader', 'style-loader', 'css-loader'], // 問題なし use: ['vue-style-loader', 'css-loader'], コンポーネント実装 Vue3 の <script setup> 構文を使って書いています。 <script setup lang="ts"> import { Swiper, SwiperSlide } from 'swiper/vue'; import 'swiper/css'; const images = [ 'https://i.picsum.photos/id/925/600/400.jpg?hmac=VxvBfviRrdHXsJ6_nbZ00wdqs0AEutxmI1ughFU8KQU', 'https://i.picsum.photos/id/428/600/400.jpg?hmac=grP0p8M-3MohHSsDfu7ZXo9VGbL-OUSCDScCUZu8ps4', 'https://i.picsum.photos/id/340/600/400.jpg?hmac=fUPfZKTRNEKns8xR8vPzDByTCfY9cEbh5lFLG8rP8H8', 'https://i.picsum.photos/id/702/600/400.jpg?hmac=KxRAc7VlNyemdt4RTHqjBvy9heWN7FV0qHG_U2OyOjo', ]; // Swiperのインスタンスが返ってくる const onSwiper = (swiper) => { console.log('swiper', swiper); }; // スライド位置が変更された時に呼ばれる const onSlideChange = () => { console.log('slide change'); }; </script> <template> <div class="story-visuals"> <swiper @swiper="onSwiper" @slideChange="onSlideChange"> <swiperSlide v-for="(image, index) in images" :key="`slide-${index}`"> <img :src="image" /> </swiperSlide> </swiper> </div> </template> 今回は拍子抜けするくらいあっさりと実装できました。 ナビゲーション、ページネーションを実装する ナビゲーション、ページネーション機能の追加も簡単です。 <script setup lang="ts"> import { Swiper, SwiperSlide } from 'swiper/vue'; import { Navigation, Pagination, Virtual } from 'swiper'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; 〜略〜 </script> <template> <div class="story-visuals"> <swiper :modules="[Navigation, Pagination, Virtual]" navigation :pagination="{ clickable: true }" virtual @swiper="onSwiper" @slideChange="onSlideChange" > <swiperSlide v-for="(image, index) in images" :virtualIndex="index" :key="`slide-${index}`"> <img :src="image" /> </swiperSlide> </swiper> </div> </template> 解説 import { Navigation, Pagination, Virtual } from 'swiper'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; ナビゲーション、ページネーションのモジュール、それぞれの CSS を読み込んでいます。 Virtual を付けると描画が Vue.js を処理するように変更されるようです。 Virtual Slides rendering here is fully handled by Vue.js and not required anything except setting :virtual="true" property and setting virtualIndex on slides: <swiper :modules="[Navigation, Pagination, Virtual]" navigation :pagination="{ clickable: true }" virtual @swiper="onSwiper" @slideChange="onSlideChange" > <swiperSlide v-for="(image, index) in images" :virtualIndex="index" :key="`slide-${index}`"> <img :src="image" /> </swiperSlide> </swiper> 追加したモジュールを modules 属性で登録しています。 有効化のために navigation、pagination、virtual 属性を付与しています。 pagination 属性ではバレット(ページネーションの●)のクリック可を指定してます。 ナビゲーション、ページネーションのデザインをカスタマイズする 標準のCSSをコメントアウトして、独自CSSを追加しています。 設定が追加されて <swiper> が読みづらくなるので、設定を変数に書き出しました。 Virtual が有効だと上手く動作しないので外しました。 <script setup lang="ts"> import { Navigation, Pagination } from 'swiper'; import { Swiper, SwiperSlide } from 'swiper/vue'; import 'swiper/css'; // 標準CSSは除外 // import 'swiper/css/navigation'; // import 'swiper/css/pagination'; // 〜略〜 const swiperModules = [Navigation, Pagination]; // ナビゲーションで使うエレメントのクラスを指定 const swiperNavigation = { prevEl: '.story-navi--prev', nextEl: '.story-navi--next', }; // ページネーションで使うエレメントのクラスを指定 // 画像をサムネイルとして並べたいのでバレットの内容を指定 const swiperPagination = { el: '.story-thumbs', clickable: true, renderBullet: (index, className) => `<span class="story-thumbs__item ${className} 1"><img src="${images[index]}" style="width:100%"></span>`, }; </script> <!-- スタイルシート --> <style> .story-visuals { width: 600px; } .story-thumbs { display: grid; grid-template-columns: repeat(4, 1fr); } <!-- ページネーションのアクティブ以外は透明度 0.4 --> .story-thumbs__item { opacity: 0.4; } .story-thumbs__item.swiper-pagination-bullet-active { opacity: 1; } </style> <template> <div class="story-visuals"> <swiper :modules="swiperModules" :pagination="swiperPagination" :navigation="swiperNavigation" :loop="true" @swiper="onSwiper" @slideChange="onSlideChange" > <swiperSlide v-for="(image, index) in images" :key="`slide-${index}`"> <img :src="image" /> </swiperSlide> </swiper> <!-- サムネイル一覧 --> <div class="story-thumbs" /> <!-- 左右移動 --> <span class="story-navi story-navi--prev">戻る</span> <span class="story-navi story-navi--next">進む</span> </div> </template> 解説 const swiperPagination = { el: '.story-thumbs', clickable: true, renderBullet: (index, className) => `<span class="story-thumbs__item ${className} 1"><img src="${images[index]}" style="width:100%"></span>`, }; renderBullet はページネーションで使用するHTMLを記載します。 画像をサムネイルとして並べたかったので index で何番目かを取り出して画像取得に利用しています。 className にはアクティブなバレットのクラス swiper-pagination-bullet-active が入ります。 <swiper :modules="swiperModules" :pagination="swiperPagination" :navigation="swiperNavigation" :loop="true" @swiper="onSwiper" @slideChange="onSlideChange" > 変数に書き出した設定を指定しています。 loop はカルーセルの無限ループです。 @swiper、@slideChange はデバッグ用なので不要なら消してOKです。 おわり Swiper は最初のとっかかりがわかりにくいのですが、そこを乗り越えれば大変便利なライブラリです。 この記事が一助になれば嬉しいです。
- 投稿日:2022-02-14T19:23:29+09:00
【vue-cli+Vuetify】HTML要素の横スクロールバーを上下に表示する
やりたいこと vue-cli+Vuetify環境下にて、 HTML要素の横スクロールバーを下部だけでなく上部にも表示したい。 完成品 実現方法 以下の通りに実装しました。※下述の参考記事から発想を得ました。 横スクロールバーをつけたいHTML要素(以下、「コンテンツ」)に CSSで下部の横スクロールバーを表示させる コンテンツの上に新しいブロック要素を追加し、 [1]と同様にCSSで下部の横スクロールバーを表示させる コンテンツと[2]で追加した要素のスクロールイベントを Vuetifyの v-scroll.self で拾う コンテンツと[2]で追加した要素の element.scrollLeft を [3]のイベント内で同期させる template <template> <div> <!-- 横スクロールバー上 --> <div id="topbar" class="scroll-x-topbar" v-scroll.self="onScrollXTopBar"> <div class="inner-topbar"></div> </div> <!-- コンテンツ + 横スクロールバー下 --> <div id="content" class="scroll-x-content" v-scroll.self="onScrollXContent"> <div class="inner-content"> これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。これはサンプルです。 </div> </div> </div> </template> script export default { methods: { onScrollXTopBar(e) { document.getElementById("content").scrollTo(e.target.scrollLeft, 0); }, onScrollXContent(e) { document.getElementById("topbar").scrollTo(e.target.scrollLeft, 0); }, }, }; style .scroll-x-topbar { overflow-y: hidden; overflow-x: scroll; width: 500px; height: 10px; } .scroll-x-content { overflow-y: hidden; overflow-x: scroll; width: 500px; } .inner-topbar { width: 1000px; height: 1px; } .inner-content { width: 1000px; background-color: lightskyblue; } 参考記事 以下の記事を類似の一例として参考にさせていただきました。ありがとうございました。
- 投稿日:2022-02-14T16:52:30+09:00
【Vue.js】親コンポーネントから子コンポーネントのメソッドの呼び出し方
親コンポーネントから子コンポーネントのメソッドの呼び出し方について簡単にメモしておく。 ちなみに、逆に子コンポーネントから親コンポーネントのメソッドを呼び出す場合は、$emitを使用する。 子コンポーネントのメソッドの呼び出し方 基本的な書き方 子コンポーネントのメソッドを呼び出すポイントは以下の2点になる。 ① 子コンポーネントに任意の名前をref属性を使って付ける。 <sample ref="child"/> ② $refsを使って子コンポーネントのメソッドを実行する。 this.$refs.child.testMethod(); 上記のように書くことで、子コンポーネントのtestMethod()が実行される。 サンプル 親コンポーネントから子コンポーネントのメソッドを呼び出す簡単なサンプルを書いてみた。 親コンポーネントのボタンが押されると子コンポーネントのclear()が呼ばれるようになっている。 親コンポーネント <template> <div> <text-box ref="textBox" /> <button @click="textClear">クリア</button> </div> </template> <script> import TextBox from "./components/TextBox.vue"; export default { name: "App", components: { TextBox, }, methods: { textClear() { this.$refs.textBox.clear(); }, }, }; </script> 子コンポーネント <template> <div> <h2>名前:</h2> <input type="text" v-model="playerName" /> </div> </template> <script> export default { name: "text-box", data() { return { playerName: "", }; }, methods: { clear() { this.playerName = ""; }, }, }; </script>
- 投稿日:2022-02-14T08:16:47+09:00
【ストック推奨】Webフロントエンドパフォーマンスチューニング55選
こんにちは、ぬこすけです。 近年、Webフロントエンドではサイトのパフォーマンスの重要性が高まっています。 例えば、GoogleはCore Web Vitalというパフォーマンスに指標を検索結果のランキング要因に組み込みました。 また、近年の某企業が「パフォーマンスの改善に取り組んだ結果、セッション数〇%アップ、CVR〇%アップ...」などの事例は枚挙にいとまがないでしょう。 パフォーマンスチューニングするためには、定量的に計測してボトルネックを探すようなトップダウンなアプローチもあります。 しかしながら、時には千本ノック的にハウツーを片っ端から試していくボトムアップなアプローチも有効になることもあったり、日々のコーディングでパフォーマンスを意識したコードを書くことは大切でしょう。 この記事ではパフォーマンス最適化のハウツーを紹介します。 パフォーマンス改善の施策が思い浮かばない時やフロントエンドのスキルを磨きたい時に辞書的な役割を果たせれば良いかなーと思っています。 ※私は55選書いている所で燃え尽きました。私自身も今後も更新する予定ですが、この記事は皆さんで作り上げたいと思っています。誤りや他にもこういうのあるよ!、この記事にわかりやすい説明あるよ!などあれば編集リクエスト、またはコメントいただけると幸いです。 ※この記事を読んでいる方にはこれからフロントエンジニアになりたい方、駆け出しエンジニアの方もいると思います。正直、何言ってるかわからない部分が結構あると思います。ですが、私の経験則上、「あの時書いてあったことはこういうことか!」と後々になって理解することがよくありました。今はよくわからないかもしれませんが、とりあえずストックなりしておいて、数ヶ月後にこの記事を見返すとまた理解度も変わるのかなーと思います。 注意事項 一口にフロントエンドといっても、SSRやらSSGやらでサーバー側も関わってくることもあるので、バックエンド寄りも話も混じっているので悪しからず。 わかりやすくするためにカテゴリに分けしていますが、微妙なカテゴリ分けのものもあるので悪しからず。 中には具体的なハウツーというより考え方みたいなものも混じっているかもしれませんが悪しからず。 環境によって必ずしもパフォーマンスが改善されるとは限らないので悪しからず。 あくまでパフォーマンスの観点なので他の観点では最適となるとは限らないので悪しからず。例えば、IndexedDBを紹介していますが、Sarafi 15で脆弱性が見つかっています。 紹介するものには特定のブラウザでしかサポートされていないものもあるので悪しからず。 JavaScript編 複数の非同期処理はPromise.allを使う もし互いに依存関係のない複数の非同期処理を実行しているのならば、Promise.allを使うのも手です。 async function notUsePromiseAll() { console.log('Start!!'); const response1 = await fetch("https://example.com/api/1"); const response2 = await fetch("https://example.com/api/2"); console.log('End!!'); } async function usePromiseAll() { console.log('Start!!'); const [response1, response2] = await Promise.all([ fetch("https://example.com/api/1"), fetch("https://example.com/api/2"), ]); console.log('End!!'); } Promise.allはいずれかの非同期処理が失敗すると全ての非同期処理が中断されます。 中断されたくない場合はPromise.allSettledが使えます。 非同期処理を待たなくて良い場合は待たない コードを眺めてみて、非同期処理を待たなくて良いところは待たないようにしましよう。 具体的には、もしasync/await構文を使っているならawaitを使わないことです。 const sendErrorToServer = async (message) => { // サーバーにエラー情報を送る処理 }; console.log('何かエラーが起きた'); // 後続の処理はサーバーにエラー情報を送る処理とは関係ないので await をつけない sendErrorToServer('エラーです'); console.log('後続の処理'); 先に非同期処理を走らせておく 互いに依存関係のある複数の非同期処理を実行する場合でも、時間がかかる処理の方を先に走らせておくのも良いでしょう。 const response1Promise = requestLongTime(); // ... // 色々処理 // ... const response1 = await response1Promise; const response2 = await requestShortTime(); console.log(response1, response2); キー/バリューを頻繁に追加や削除する場合はMapを使う MDNにも記載がありますが、キー/バリューのペアを頻繁に追加や削除する場合はObjectよりもMapを使ったほうが最適です。 const nameAgeMap = new Map() nameAgeMap.set('Tom', 19) nameAgeMap.set('Nancy', 32) nameAgeMap.delete('Tom') nameAgeMap.delete('Nancy') ... 膨大な配列の検索はキー/バリューで JavaScriptというよりかはロジックの問題かもしれません。 膨大な配列を検索する場合はキー/バリューに変換してから検索した方が速いです。 const thousandsPeople = [ { name: 'Tom', age: 19 }, { name: 'Nancy', age: 32 }, // ...めちゃくちゃ多い ] // 時間かかる const myFriend = thousandsPeople.find(({ name }) => name === 'Tom'); console.log(`The age is ${myFriend.age}`); const thousandsPeopleMap = { 'Tom': 19, 'Nancy': 32, // ... } // こっちのほうが速い const myFriend2 = thousandsPeopleMap['Tom']; console.log(`The age is ${myFriend2.age}`); 関数の結果をキャッシュする 頻繁に同じ引数で関数を実行したり、重い処理を走らせるなら関数の結果をキャッシュするのも有効です。 次のようなデコレータ関数を作れば、関数の結果をキャッシュできます。 function cachingDecorator(func) { const cache = new Map(); return x => { if (!x) { return func(x) } if (cache.has(x)) { return cache.get(x); } const result = func(x); cache.set(x, result); return result; } } function heavyFuncNoCache(str) { // 重い処理 } const heavyFunc = cachingDecorator(heavyFuncNoCache); heavyFunc('hoge'); // キャッシュから結果が返却される heavyFunc('hoge'); requireではなくimportを使う JavaScriptのモジュールの読み込み方にはrequireとimportの2種類があります。 requireは同期的、importは非同期的にモジュールを読み込むので、importの方が良いでしょう。 Node.jsといったサーバーサイドでJavaScriptを記述する場合はrequireを使うことが多いと思いますが、バージョン14であればpackage.jsonだったりファイルの拡張子をmjsにしたりいじることでimportで読み込めます。 なお、Qiitaのこの記事がわかりやすいです。 フェッチにはKeep-Aliveを指定する 何度も同じドメインへアクセスするのであればkeep-aliveを指定することでフェッチ処理が短縮されます。 import axios from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; const httpAgent = new HttpAgent({ keepAlive: true }); const httpsAgent = new HttpsAgent({ keepAlive: true }); const keepAliveAxios = axios.create({ httpAgent, httpsAgent, }); keepAliveAxios.get(...); 非同期の関数を使う Node.jsには同期/非同期で別で用意されている関数があったりします。 例えばファイルに書き込みをする関数にはfs.writeFileSyncとfs.writeFileがあります。 もしフロントエンドアプリケーションのビルド時などに静的ファイルを生成する必要がある場合、特段理由がなければfs.writeFileを使いましょう。 HTML/CSSなどリソース編 imgやiframe、linkタグなどにimportance属性を追加する imgやiframe、linkタグなどではimportance属性を使うことでブラウザに読み込みの優先度を指定できます。 タグだけでなくfetch関数でもオプションでimportanceを指定できたりします。 imgやiframeタグにloading属性を追加する imgやiframeタグにはloading属性を使うことで読み込みのタイミングを指定できます。 もし、遅延/非同期読み込みしたい場合はloading='async'を使うと良いでしょう。 ただし、ファーストビューに使うと返って読み込みが遅くなる可能性もあるので注意しましょう。 imgタグにdecoding属性を追加する imgタグはdecoding属性を使うことでデコードを同期/非同期的に読み込むかを指定できます。 decoding='async'を指定すれば非同期的にデコード処理をブラウザに指示できます。 imgタグにはサイズを指定しておく imgタグのwidth/height属性などを使って、画像のサイズを指定しておきましょう。ブラウザのレンダリングの助けになります。 CLSの改善にも繋がります。 わからない場合は大体のサイズを指定しましょう。 優先度の高いリソースはlinkタグにpreloadを指定する ファーストビューに表示する画像など、優先度の高いリソースはlinkタグのrel属性にpreloadを指定ことで速い読み込みが期待できます。 優先度の高い外部ドメインへのアクセスがある時はlinkタグにdns-prefetchまたはpreconnectを指定する 外部ドメインからリソースを取得したり重要度の高い外部リンクを設置している場合などは、linkタグのdns-prefetchやpreconnectが使えます。 dns-prefetchはDNSルックアップ、preconnectは事前接続まで行います。 かなり優先度の高い外部ドメインへのアクセスはpreconnect、少し優先度が落ちる場合はdns-prefetchを使うと良いでしょう。 ユーザーがよく遷移するページはlinkタグにprerenderを指定する linkタグのrel属性にprerenderを指定することで、ブラウザは指定されたページをバック グラウンドでレンダリングします。 なので、ユーザーが指定されたページへ遷移する時はすぐに画面表示ができます。 ユースケースとしては、ランキングサイトのようなページで1位へのページへ遷移するユーザーは多いので、prerenderを指定しておくと良いかもしれません。 ただし、レンダリングされる都合上、ブラウザへの負荷が高かったり、JavaScriptで仕込んでいる計測処理が発火するなどの注意は必要です。 scriptタグにdeferやasync属性を追加する ブラウザでスクリプトが読み込まれるとHTMLやCSSの解析がブロックされます。 このような問題を解決するためにdeferやasync属性が使えます。 deferはHTMLやCSSの解析をブロックすることなくスクリプトを読み込んでおき、解析が完了したらスクリプトを実行します。 asyncはHTMLやCSSの解析とは独立してスクリプトの読み込み・実行をします。 Qiitaのこの記事がわかりやすいです。 優先度の高いリソースの読み込みはできるだけHTML上部で定義する ブラウザはHTMLドキュメントの上から解釈してきます。 なので、例えば同じpreloadを指定しているリソースでも、さらに優先度の高いものはよりHTML上部に定義して早めにブラウザが読み込めるようにしましょう。 CSSで余計なセレクタは書かない ブラウザはCSSセレクタを右から左に解析します。 なので、できる限り単一のクラス名やid名で指定した方が解析のスピードが上がります。 /* ブラウザは全てのdivタグを探し、さらに上の階層のhogeクラスを見つけようと解析する */ .hoge div {} /* Best Practice */ .hoge {} #hoge {} style属性を使って直接スタイルを指定する クラスなどセレクタを指定してCSSを書くよりも、直接HTMLタグのstyle属性を使ったほうがブラウザの解析は速いです。 ただし、コードの可読性やメンテが厳しくはなります。 <div style='color: red;'>ほげ</div> 不要なCSSを削除する 使っていないCSSは削除しましょう。 Chromeのデベロッパーツールを使えば不要なCSSを洗い出すことができます。 不要なJavaScriptを削除する 使っていないJavaScriptは削除しましょう。 例えば、console.logは基本的にプロダクションのコードでは不要なので、eslintで検出するなりbabelで削除するなりします。 ファーストビューに影響のあるCSSはheadタグの先頭で読み込む JavaScriptと違い、ブラウザのCSSの解析はHTMLの解析をブロックしません。 ファーストビューで読み込ませたいCSSはできるだけheadタグの先頭に読み込ませて、速くスタイリングされたファーストビューをユーザーに見せるようにしましょう。 ファーストビューに影響のないCSSはbodyタグの末尾で読み込む 逆にファーストビューに影響のないCSSはbodyタグの末尾で読み込ませることで、ブラウザにCSSの読み込みを遅延させます。 JavaScriptはbodyタグの末尾で読み込む ブラウザはJavaScriptの解析を始めるとHTMLやCSSの解析をストップします。 なので、JavaScriptはbodyタグの末尾で読み込み、HTMLやCSSの解析が終わった後のJavaScriptを解析するようにしましょう。 ただし、Google Analyticsなどの解析用のJavaScript等は除きます。 HTMLやCSS、JSをMinify/バンドルする Webpackやswcなどのバンドラーを使いましょう。 JavaScriptのトランスパイルを最新のESに合わせる もしJavaScriptをES2015でトランスパイルしている場合は、それよりも最新のバージョンでトランスパイルすることによって、JavaScriptのサイズを落とすことができます。 ただし、IEといった古いブラウザを切り捨てる覚悟は必要です。 画像はWebPやAVIFを使う 次世代の画像フォーマットとしてWebPやAVIFがあります。 こららの画像フォーマットを使うことで従来のPNG等の形式よりも画像サイズを縮小できたりします。 IKEAではAVIFによって画像の転送量を21.4%削減した例もあります。 画像サイズを縮小する 画質を落とすなり幅/高さを小さくするなりして画像サイズを縮小させます。 例えば、SVGでは作成したツールによってはコメントアウトが残っていたりで最適化されずに出力されている場合もあるので、手動で削除するなりツールを使うなりで縮小させます。 画像をインライン化する インライン画像としてHTMLに直接埋め込むことで、画像のリクエスト数を抑えることができます。 ただし、画像サイズが大きくなったりブラウザのキャッシュが効かない等のデメリットはあります。 画像サイズが小さく、一度しか読み込まれない場合などに有効といわれています。 過大なDOMを避ける DOMが多すぎるとブラウザの描画に負担をかけてしまいます。 不要なDOMを削除するのはもちろん、遅延読み込みや仮想無限スクロールなどを駆使してユーザーに表示されている部分だけ描画することで対策できます。 ブラウザAPI編 永続化ストレージはLocalStorageよりIndexedDBを使う ブラウザの永続化ストレージにはLocalStorageとIndexedDBが使えます。 LocalStorageは同期的、IndexedDBは非同期処理なので、IndexedDBの方がブラウザの動きを阻害することなくデータアクセスができます。 重たい処理やUIに依存しない処理はWebWorkerを使う WebWorkerを使うことでブラウザのメインスレッドとは別のスレッド立ち上げることができます。 フロントで検索機能といった重たい処理だったり、エラーをサーバーに送信するといったUIに依存しない処理はWebWorkerを使うことでメインスレッドの処理を阻害させません。 ServiceWorkerでリソースをキャッシュする ServiceWorker といえばPWA(Progressive Web Application)のイメージが強いですが、ブラウザから外部サーバーへのリクエストをフックしてHTMLやCSS、JSなどのリソースをキャッシュすることができます。 リクエストする際はキャッシュから取得することができるので外部サーバーへのリクエストするよりも処理が速くなります。 また、キャッシュから取得するか、先にサーバーへデータ取得してからキャッシュするかなど柔軟なキャッシュ戦略を選択できます。 ServiceWorkerを使う時はNavigationPreloadsも使う サイトにアクセス時、必要なリソースをフェッチする時にはServiceWorkerが起動するのを待ってフェッチ処理が走ります。 NavigationPreloadsではServiceWorkerの起動を待たずフェッチ処理を開始することができます。 WebAssembly を使う JavaScriptだけでなく、CやRustで書いたコードがブラウザで実行でき、JavaScriptよりも高速化される場合があります。 Amazonの事例もあります。 V8エンジン編 ChromeやNode.jsでは内部的にV8エンジンが使われています。 ここまで最適化すると変態ですが、チップスとして紹介します。 値の格納はコンストラクタで V8エンジンでは内部的にhidden classというものを生成します。 詳しい仕組みは割愛しますが、インスタンス化したオブジェクトに対して値を追加すると、新しいhidden classが生成されてしまいます。 class Point { constructor(x, y) { this.x = x; this.y = y; } } var p1 = new Point(11, 22); // hidden class の生成 var p2 = new Point(33, 44); // hidden class の再利用 p1.z = 55; // hidden class が生成されてしまう オブジェクトは同じ順番のプロパティで生成する これもhidden classに関わる話ですが、違う順番でプロパティを生成すると新たにhidden classが生成されます。 const obj = { a: 1 }; obj.b = 2 // hidden classを使い回せる const obj2 = { a: 1 }; obj2.b = 2 // 新しいhidden classが生成されてしまう const obj3 = { b: 2 }; obj3.a = 1 関数は同じ引数の型を使う 関数の引数はできるだけ同じ型を使うようにします。 function add(x,y) { return x + y } add(1,2) add(3,4) // OK add('3','4') // NG クラスはトップレベルのスコープで定義する 関数内でクラスを定義するのはV8エンジン的には良くないそうです。 // NG function createPoint(x, y) { class Point { constructor(x,y) { this.x = x this.y = y } } return new Point(x,y) } ライブラリ編 軽量なライブラリを採用する ライブラリを採用する1つの観点としてサイズがあります。 bundlephobia というサイトでライブラリのサイズをチェックすることができます。 ライブラリのサイズを減らす moment.js や lodash などのライブラリはWebpackのプラグインを使って不必要なスクリプトを削減することができます。 ライブラリのドキュメントを読む ライブラリの公式ドキュメントには最適化のTipsが載っていたりします。 例えば、Reactにはパフォーマンス最適化、TailwindCSSにはOptimizing for Productionというページが公式のドキュメントに記載されています。 各ライブラリのドキュメントをしっかり見てみましょう。 ライブラリに頼らず自前で作る ライブラリは万人向けに最適化されており、あなたのアプリケーション向けには最適化されていません。 あなたのアプリケーション以上に機能過多であることがほとんどです。 時には自前で作るのも1つの手です。 SPA編 ReactやVueといったコンポーネント志向のライブラリを想定しています。 Reactのコード例が多いですが、Vueでも参考になるかと思います。 コンポーネントがマウントされた後、遅延的にデータを読み込みする 優先順位だったりデータサイズが大きい場合等はマウント後リソースを取得します。 // 先にimportしない // import articles from './articles.json'; function ArticlesComponent() { const [articles, setArticles] = useState([]); // マウント後にデータを読み込む useEffect(() => { import('./articles.json').then(res => setArticles(res.default)); }, []) return articles.map(article => <div key={article.id}>{article.title}</div>) } コンポーネントを遅延読み込みする 初めてコンポーネントが表示されるタイミングでコンポーネントを読み込みます。 例えば、ユーザーがボタンをタップして初めて表示されるコンポーネントは遅延読み込みでの実装を考えます。 Reactで言えばSuspense、Next.jsならdyamicのAPIを使ってコンポーネントの遅延読み込みを実装できます。 SSRやSSG、ISRに移行する ReactやVueなど通常のSPAは性質上、初期描画が遅くなります。 ReactであればNext.jsやGatsuby.js、VueであればNuxt.jsといったフレームワークを使えば初期描画が遅くなる問題を解決できます。 コンポーネントの設計を最適化する ReactやVueだとコンポーネントのレンダリングの仕組みが違うので一概にこれが最適とは言えませんが、共通した設計の最適化があります。 例えば、「コンポーネントとデータの依存を考えて、再レンダリングの範囲を最小限にする」ことでしょう。 次のコンポーネントの例を見てください。 <!-- とあるコンポーネント --> <div> <div>データAに依存するUI部分</div> <div>データAに依存しないUI部分</div> </div> 1つのコンポーネント内に「データAに依存するUI部分」と「データAに依存しないUI部分」があります。 ReactであれVueであれこのようなケースの場合は「データAに依存しないUI部分」を別コンポーネントに切り出したほうが良いでしょう。 そうすればデータAに変更があった時、「データAに依存するUI部分」のみ再レンダリングさせることができます。 (Vueであれば問題ないですが、Reactの場合はステート管理のライブラリを使っていない場合はReact.memoを使う必要はあります) サーバー編 必要なデータのみフロントへ返却する 例えば、記事の一覧ページに各記事の本文を一部表示するとします。 「本文を一部」だけならサーバーからは一部だけ返却するようにします。 そうすることでファイルサイズ削減などができます。 事前に静的ファイルにしておく 都度APIへアクセスするのであれば予めJsonにしておくのも良いでしょう。 日本にあるサーバーを使う 日本向けのアプリを開発しているのであれば、地理的に近い日本のサーバーを選びましょう。 Brotli圧縮を使う gzipよりは圧縮後のサイズ削減や圧縮速度の向上が見込めます。 CDNを使う Amazon CloudFrontなどのCDNはできるなら使いましょう。 HTTP/2を使う できるなら使いましょう。HTTP/1.1より速いです。 HTTPキャッシュを使う Cache-ControlなどのHTTPヘッダーを利用して、ブラウザにリソースをキャッシュさせます。 まとめ この記事では次のようにカテゴリ分けしてWebフロントエンドのパフォーマンスチューニングのハウツーを紹介しました。 JavaScript編 HTML/CSSなどリソース編 ブラウザAPI編 V8エンジン編 ライブラリ編 SPA編 皆さんのパフォーマンスチューニング力の力添えになれば幸いです!
- 投稿日:2022-02-14T08:16:47+09:00
Webフロントエンドパフォーマンスチューニング55選
こんにちは、ぬこすけです。 近年、Webフロントエンドではサイトのパフォーマンスの重要性が高まっています。 例えば、GoogleはCore Web Vitalというパフォーマンスに指標を検索結果のランキング要因に組み込みました。 また、近年の某企業が「パフォーマンスの改善に取り組んだ結果、セッション数〇%アップ、CVR〇%アップ...」などの事例は枚挙にいとまがないでしょう。 パフォーマンスチューニングするためには、定量的に計測してボトルネックを探すようなトップダウンなアプローチもあります。 しかしながら、時には千本ノック的にハウツーを片っ端から試していくボトムアップなアプローチも有効になることもあったり、日々のコーディングでパフォーマンスを意識したコードを書くことは大切でしょう。 この記事ではパフォーマンス最適化のハウツーを紹介します。 パフォーマンス改善の施策が思い浮かばない時やフロントエンドのスキルを磨きたい時に辞書的な役割を果たせれば良いかなーと思っています。 ※私は55選書いている所で燃え尽きました。私自身も今後も更新する予定ですが、この記事は皆さんで作り上げたいと思っています。誤りや他にもこういうのあるよ!、この記事にわかりやすい説明あるよ!などあれば編集リクエスト、またはコメントいただけると幸いです。 ※この記事を読んでいる方にはこれからフロントエンジニアになりたい方、駆け出しエンジニアの方もいると思います。正直、何言ってるかわからない部分が結構あると思います。ですが、私の経験則上、「あの時書いてあったことはこういうことか!」と後々になって理解することがよくありました。今はよくわからないかもしれませんが、とりあえずストックなりしておいて、数ヶ月後にこの記事を見返すとまた理解度も変わるのかなーと思います。 注意事項 一口にフロントエンドといっても、SSRやらSSGやらでサーバー側も関わってくることもあるので、バックエンド寄りも話も混じっているので悪しからず。 わかりやすくするためにカテゴリに分けしていますが、微妙なカテゴリ分けのものもあるので悪しからず。 中には具体的なハウツーというより考え方みたいなものも混じっているかもしれませんが悪しからず。 環境によって必ずしもパフォーマンスが改善されるとは限らないので悪しからず。 あくまでパフォーマンスの観点なので他の観点では最適となるとは限らないので悪しからず。例えば、IndexedDBを紹介していますが、Sarafi 15で脆弱性が見つかっています。 紹介するものには特定のブラウザでしかサポートされていないものもあるので悪しからず。 JavaScript編 複数の非同期処理はPromise.allを使う もし互いに依存関係のない複数の非同期処理を実行しているのならば、Promise.allを使うのも手です。 async function notUsePromiseAll() { console.log('Start!!'); const response1 = await fetch("https://example.com/api/1"); const response2 = await fetch("https://example.com/api/2"); console.log('End!!'); } async function usePromiseAll() { console.log('Start!!'); const [response1, response2] = await Promise.all([ fetch("https://example.com/api/1"), fetch("https://example.com/api/2"), ]); console.log('End!!'); } Promise.allはいずれかの非同期処理が失敗すると全ての非同期処理が中断されます。 中断されたくない場合はPromise.allSettledが使えます。 非同期処理を待たなくて良い場合は待たない コードを眺めてみて、非同期処理を待たなくて良いところは待たないようにしましよう。 具体的には、もしasync/await構文を使っているならawaitを使わないことです。 const sendErrorToServer = async (message) => { // サーバーにエラー情報を送る処理 }; console.log('何かエラーが起きた'); // 後続の処理はサーバーにエラー情報を送る処理とは関係ないので await をつけない sendErrorToServer('エラーです'); console.log('後続の処理'); 先に非同期処理を走らせておく 互いに依存関係のある複数の非同期処理を実行する場合でも、時間がかかる処理の方を先に走らせておくのも良いでしょう。 const response1Promise = requestLongTime(); // ... // 色々処理 // ... const response1 = await response1Promise; const response2 = await requestShortTime(); console.log(response1, response2); キー/バリューを頻繁に追加や削除する場合はMapを使う MDNにも記載がありますが、キー/バリューのペアを頻繁に追加や削除する場合はObjectよりもMapを使ったほうが最適です。 const nameAgeMap = new Map() nameAgeMap.set('Tom', 19) nameAgeMap.set('Nancy', 32) nameAgeMap.delete('Tom') nameAgeMap.delete('Nancy') ... 膨大な配列の検索はキー/バリューで JavaScriptというよりかはロジックの問題かもしれません。 膨大な配列を検索する場合はキー/バリューに変換してから検索した方が速いです。 const thousandsPeople = [ { name: 'Tom', age: 19 }, { name: 'Nancy', age: 32 }, // ...めちゃくちゃ多い ] // 時間かかる const myFriend = thousandsPeople.find(({ name }) => name === 'Tom'); console.log(`The age is ${myFriend.age}`); const thousandsPeopleMap = { 'Tom': 19, 'Nancy': 32, // ... } // こっちのほうが速い const myFriend2 = thousandsPeopleMap['Tom']; console.log(`The age is ${myFriend2.age}`); 関数の結果をキャッシュする 頻繁に同じ引数で関数を実行したり、重い処理を走らせるなら関数の結果をキャッシュするのも有効です。 次のようなデコレータ関数を作れば、関数の結果をキャッシュできます。 function cachingDecorator(func) { const cache = new Map(); return x => { if (!x) { return func(x) } if (cache.has(x)) { return cache.get(x); } const result = func(x); cache.set(x, result); return result; } } function heavyFuncNoCache(str) { // 重い処理 } const heavyFunc = cachingDecorator(heavyFuncNoCache); heavyFunc('hoge'); // キャッシュから結果が返却される heavyFunc('hoge'); requireではなくimportを使う JavaScriptのモジュールの読み込み方にはrequireとimportの2種類があります。 requireは同期的、importは非同期的にモジュールを読み込むので、importの方が良いでしょう。 Node.jsといったサーバーサイドでJavaScriptを記述する場合はrequireを使うことが多いと思いますが、バージョン14であればpackage.jsonだったりファイルの拡張子をmjsにしたりいじることでimportで読み込めます。 なお、Qiitaのこの記事がわかりやすいです。 フェッチにはKeep-Aliveを指定する 何度も同じドメインへアクセスするのであればkeep-aliveを指定することでフェッチ処理が短縮されます。 import axios from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; const httpAgent = new HttpAgent({ keepAlive: true }); const httpsAgent = new HttpsAgent({ keepAlive: true }); const keepAliveAxios = axios.create({ httpAgent, httpsAgent, }); keepAliveAxios.get(...); 非同期の関数を使う Node.jsには同期/非同期で別で用意されている関数があったりします。 例えばファイルに書き込みをする関数にはfs.writeFileSyncとfs.writeFileがあります。 もしフロントエンドアプリケーションのビルド時などに静的ファイルを生成する必要がある場合、特段理由がなければfs.writeFileを使いましょう。 HTML/CSSなどリソース編 imgやiframe、linkタグなどにimportance属性を追加する imgやiframe、linkタグなどではimportance属性を使うことでブラウザに読み込みの優先度を指定できます。 タグだけでなくfetch関数でもオプションでimportanceを指定できたりします。 imgやiframeタグにloading属性を追加する imgやiframeタグにはloading属性を使うことで読み込みのタイミングを指定できます。 もし、遅延/非同期読み込みしたい場合はloading='async'を使うと良いでしょう。 ただし、ファーストビューに使うと返って読み込みが遅くなる可能性もあるので注意しましょう。 imgタグにdecoding属性を追加する imgタグはdecoding属性を使うことでデコードを同期/非同期的に読み込むかを指定できます。 decoding='async'を指定すれば非同期的にデコード処理をブラウザに指示できます。 imgタグにはサイズを指定しておく imgタグのwidth/height属性などを使って、画像のサイズを指定しておきましょう。ブラウザのレンダリングの助けになります。 CLSの改善にも繋がります。 わからない場合は大体のサイズを指定しましょう。 優先度の高いリソースはlinkタグにpreloadを指定する ファーストビューに表示する画像など、優先度の高いリソースはlinkタグのrel属性にpreloadを指定ことで速い読み込みが期待できます。 優先度の高い外部ドメインへのアクセスがある時はlinkタグにdns-prefetchまたはpreconnectを指定する 外部ドメインからリソースを取得したり重要度の高い外部リンクを設置している場合などは、linkタグのdns-prefetchやpreconnectが使えます。 dns-prefetchはDNSルックアップ、preconnectは事前接続まで行います。 かなり優先度の高い外部ドメインへのアクセスはpreconnect、少し優先度が落ちる場合はdns-prefetchを使うと良いでしょう。 ユーザーがよく遷移するページはlinkタグにprerenderを指定する linkタグのrel属性にprerenderを指定することで、ブラウザは指定されたページをバック グラウンドでレンダリングします。 なので、ユーザーが指定されたページへ遷移する時はすぐに画面表示ができます。 ユースケースとしては、ランキングサイトのようなページで1位へのページへ遷移するユーザーは多いので、prerenderを指定しておくと良いかもしれません。 ただし、レンダリングされる都合上、ブラウザへの負荷が高かったり、JavaScriptで仕込んでいる計測処理が発火するなどの注意は必要です。 scriptタグにdeferやasync属性を追加する ブラウザでスクリプトが読み込まれるとHTMLやCSSの解析がブロックされます。 このような問題を解決するためにdeferやasync属性が使えます。 deferはHTMLやCSSの解析をブロックすることなくスクリプトを読み込んでおき、解析が完了したらスクリプトを実行します。 asyncはHTMLやCSSの解析とは独立してスクリプトの読み込み・実行をします。 Qiitaのこの記事がわかりやすいです。 優先度の高いリソースの読み込みはできるだけHTML上部で定義する ブラウザはHTMLドキュメントの上から解釈してきます。 なので、例えば同じpreloadを指定しているリソースでも、さらに優先度の高いものはよりHTML上部に定義して早めにブラウザが読み込めるようにしましょう。 CSSで余計なセレクタは書かない ブラウザはCSSセレクタを右から左に解析します。 なので、できる限り単一のクラス名やid名で指定した方が解析のスピードが上がります。 /* ブラウザは全てのdivタグを探し、さらに上の階層のhogeクラスを見つけようと解析する */ .hoge div {} /* Best Practice */ .hoge {} #hoge {} style属性を使って直接スタイルを指定する クラスなどセレクタを指定してCSSを書くよりも、直接HTMLタグのstyle属性を使ったほうがブラウザの解析は速いです。 ただし、コードの可読性やメンテが厳しくはなります。 <div style='color: red;'>ほげ</div> 不要なCSSを削除する 使っていないCSSは削除しましょう。 Chromeのデベロッパーツールを使えば不要なCSSを洗い出すことができます。 不要なJavaScriptを削除する 使っていないJavaScriptは削除しましょう。 例えば、console.logは基本的にプロダクションのコードでは不要なので、eslintで検出するなりbabelで削除するなりします。 ファーストビューに影響のあるCSSはheadタグの先頭で読み込む JavaScriptと違い、ブラウザのCSSの解析はHTMLの解析をブロックしません。 ファーストビューで読み込ませたいCSSはできるだけheadタグの先頭に読み込ませて、速くスタイリングされたファーストビューをユーザーに見せるようにしましょう。 ファーストビューに影響のないCSSはbodyタグの末尾で読み込む 逆にファーストビューに影響のないCSSはbodyタグの末尾で読み込ませることで、ブラウザにCSSの読み込みを遅延させます。 JavaScriptはbodyタグの末尾で読み込む ブラウザはJavaScriptの解析を始めるとHTMLやCSSの解析をストップします。 なので、JavaScriptはbodyタグの末尾で読み込み、HTMLやCSSの解析が終わった後のJavaScriptを解析するようにしましょう。 ただし、Google Analyticsなどの解析用のJavaScript等は除きます。 HTMLやCSS、JSをMinify/バンドルする Webpackやswcなどのバンドラーを使いましょう。 JavaScriptのトランスパイルを最新のESに合わせる もしJavaScriptをES2015でトランスパイルしている場合は、それよりも最新のバージョンでトランスパイルすることによって、JavaScriptのサイズを落とすことができます。 ただし、IEといった古いブラウザを切り捨てる覚悟は必要です。 画像はWebPやAVIFを使う 次世代の画像フォーマットとしてWebPやAVIFがあります。 こららの画像フォーマットを使うことで従来のPNG等の形式よりも画像サイズを縮小できたりします。 IKEAではAVIFによって画像の転送量を21.4%削減した例もあります。 画像サイズを縮小する 画質を落とすなり幅/高さを小さくするなりして画像サイズを縮小させます。 例えば、SVGでは作成したツールによってはコメントアウトが残っていたりで最適化されずに出力されている場合もあるので、手動で削除するなりツールを使うなりで縮小させます。 画像をインライン化する インライン画像としてHTMLに直接埋め込むことで、画像のリクエスト数を抑えることができます。 ただし、画像サイズが大きくなったりブラウザのキャッシュが効かない等のデメリットはあります。 画像サイズが小さく、一度しか読み込まれない場合などに有効といわれています。 過大なDOMを避ける DOMが多すぎるとブラウザの描画に負担をかけてしまいます。 不要なDOMを削除するのはもちろん、遅延読み込みや仮想無限スクロールなどを駆使してユーザーに表示されている部分だけ描画することで対策できます。 ブラウザAPI編 永続化ストレージはLocalStorageよりIndexedDBを使う ブラウザの永続化ストレージにはLocalStorageとIndexedDBが使えます。 LocalStorageは同期的、IndexedDBは非同期処理なので、IndexedDBの方がブラウザの動きを阻害することなくデータアクセスができます。 重たい処理やUIに依存しない処理はWebWorkerを使う WebWorkerを使うことでブラウザのメインスレッドとは別のスレッド立ち上げることができます。 フロントで検索機能といった重たい処理だったり、エラーをサーバーに送信するといったUIに依存しない処理はWebWorkerを使うことでメインスレッドの処理を阻害させません。 ServiceWorkerでリソースをキャッシュする ServiceWorker といえばPWA(Progressive Web Application)のイメージが強いですが、ブラウザから外部サーバーへのリクエストをフックしてHTMLやCSS、JSなどのリソースをキャッシュすることができます。 リクエストする際はキャッシュから取得することができるので外部サーバーへのリクエストするよりも処理が速くなります。 また、キャッシュから取得するか、先にサーバーへデータ取得してからキャッシュするかなど柔軟なキャッシュ戦略を選択できます。 ServiceWorkerを使う時はNavigationPreloadsも使う サイトにアクセス時、必要なリソースをフェッチする時にはServiceWorkerが起動するのを待ってフェッチ処理が走ります。 NavigationPreloadsではServiceWorkerの起動を待たずフェッチ処理を開始することができます。 WebAssembly を使う JavaScriptだけでなく、CやRustで書いたコードがブラウザで実行でき、JavaScriptよりも高速化される場合があります。 Amazonの事例もあります。 V8エンジン編 ChromeやNode.jsでは内部的にV8エンジンが使われています。 ここまで最適化すると変態ですが、チップスとして紹介します。 値の格納はコンストラクタで V8エンジンでは内部的にhidden classというものを生成します。 詳しい仕組みは割愛しますが、インスタンス化したオブジェクトに対して値を追加すると、新しいhidden classが生成されてしまいます。 class Point { constructor(x, y) { this.x = x; this.y = y; } } var p1 = new Point(11, 22); // hidden class の生成 var p2 = new Point(33, 44); // hidden class の再利用 p1.z = 55; // hidden class が生成されてしまう オブジェクトは同じ順番のプロパティで生成する これもhidden classに関わる話ですが、違う順番でプロパティを生成すると新たにhidden classが生成されます。 const obj = { a: 1 }; obj.b = 2 // hidden classを使い回せる const obj2 = { a: 1 }; obj2.b = 2 // 新しいhidden classが生成されてしまう const obj3 = { b: 2 }; obj3.a = 1 関数は同じ引数の型を使う 関数の引数はできるだけ同じ型を使うようにします。 function add(x,y) { return x + y } add(1,2) add(3,4) // OK add('3','4') // NG クラスはトップレベルのスコープで定義する 関数内でクラスを定義するのはV8エンジン的には良くないそうです。 // NG function createPoint(x, y) { class Point { constructor(x,y) { this.x = x this.y = y } } return new Point(x,y) } ライブラリ編 軽量なライブラリを採用する ライブラリを採用する1つの観点としてサイズがあります。 bundlephobia というサイトでライブラリのサイズをチェックすることができます。 ライブラリのサイズを減らす moment.js や lodash などのライブラリはWebpackのプラグインを使って不必要なスクリプトを削減することができます。 ライブラリのドキュメントを読む ライブラリの公式ドキュメントには最適化のTipsが載っていたりします。 例えば、Reactにはパフォーマンス最適化、TailwindCSSにはOptimizing for Productionというページが公式のドキュメントに記載されています。 各ライブラリのドキュメントをしっかり見てみましょう。 ライブラリに頼らず自前で作る ライブラリは万人向けに最適化されており、あなたのアプリケーション向けには最適化されていません。 あなたのアプリケーション以上に機能過多であることがほとんどです。 時には自前で作るのも1つの手です。 SPA編 ReactやVueといったコンポーネント志向のライブラリを想定しています。 Reactのコード例が多いですが、Vueでも参考になるかと思います。 コンポーネントがマウントされた後、遅延的にデータを読み込みする 優先順位だったりデータサイズが大きい場合等はマウント後リソースを取得します。 // 先にimportしない // import articles from './articles.json'; function ArticlesComponent() { const [articles, setArticles] = useState([]); // マウント後にデータを読み込む useEffect(() => { import('./articles.json').then(res => setArticles(res.default)); }, []) return articles.map(article => <div key={article.id}>{article.title}</div>) } コンポーネントを遅延読み込みする 初めてコンポーネントが表示されるタイミングでコンポーネントを読み込みます。 例えば、ユーザーがボタンをタップして初めて表示されるコンポーネントは遅延読み込みでの実装を考えます。 Reactで言えばSuspense、Next.jsならdyamicのAPIを使ってコンポーネントの遅延読み込みを実装できます。 SSRやSSG、ISRに移行する ReactやVueなど通常のSPAは性質上、初期描画が遅くなります。 ReactであればNext.jsやGatsuby.js、VueであればNuxt.jsといったフレームワークを使えば初期描画が遅くなる問題を解決できます。 コンポーネントの設計を最適化する ReactやVueだとコンポーネントのレンダリングの仕組みが違うので一概にこれが最適とは言えませんが、共通した設計の最適化があります。 例えば、「コンポーネントとデータの依存を考えて、再レンダリングの範囲を最小限にする」ことでしょう。 次のコンポーネントの例を見てください。 <!-- とあるコンポーネント --> <div> <div>データAに依存するUI部分</div> <div>データAに依存しないUI部分</div> </div> 1つのコンポーネント内に「データAに依存するUI部分」と「データAに依存しないUI部分」があります。 ReactであれVueであれこのようなケースの場合は「データAに依存しないUI部分」を別コンポーネントに切り出したほうが良いでしょう。 そうすればデータAに変更があった時、「データAに依存するUI部分」のみ再レンダリングさせることができます。 (Vueであれば問題ないですが、Reactの場合はステート管理のライブラリを使っていない場合はReact.memoを使う必要はあります) サーバー編 必要なデータのみフロントへ返却する 例えば、記事の一覧ページに各記事の本文を一部表示するとします。 「本文を一部」だけならサーバーからは一部だけ返却するようにします。 そうすることでファイルサイズ削減などができます。 事前に静的ファイルにしておく 都度APIへアクセスするのであれば予めJsonにしておくのも良いでしょう。 日本にあるサーバーを使う 日本向けのアプリを開発しているのであれば、地理的に近い日本のサーバーを選びましょう。 Brotli圧縮を使う gzipよりは圧縮後のサイズ削減や圧縮速度の向上が見込めます。 CDNを使う Amazon CloudFrontなどのCDNはできるなら使いましょう。 HTTP/2を使う できるなら使いましょう。HTTP/1.1より速いです。 HTTPキャッシュを使う Cache-ControlなどのHTTPヘッダーを利用して、ブラウザにリソースをキャッシュさせます。 まとめ この記事では次のようにカテゴリ分けしてWebフロントエンドのパフォーマンスチューニングのハウツーを紹介しました。 JavaScript編 HTML/CSSなどリソース編 ブラウザAPI編 V8エンジン編 ライブラリ編 SPA編 皆さんのパフォーマンスチューニング力の力添えになれば幸いです!
- 投稿日:2022-02-14T07:09:23+09:00
【個人開発】Twitterで映える!ファンレターを交換・シェアして楽しめるサービス『ご縁箱』をリリースしました✧˖°。
はじめに... ▼ 皆さんはTwitterを使う上で このように感じたことはありませんか? 『 今ある ご縁を大切にしたい 』 『 フォロワーさんと感謝の気持ちを伝えたり、交流を深めたい! 』 『 あの人に話しかけてみたい! 』 けどきっかけがない..ちょっと億劫かも... こういった問題も楽しみながら解決出来る『 ご縁箱 』というサービスを開発しました。(Rails × Vue.js) ▼ ご縁箱を使うとどうなる? 実際に使ってみて... まだ話したことがなかったフォロワーさんと話すきっかけになった 自分の率直な気持ちをレターを送って伝えることで喜んでもらえた ご飯の約束ができた(全部焼き肉) ありがたいことにリリースから約3週間弱で総PV数が19,000を超え、多くの方に継続して使っていただいております?♂️ 【サービスURL】 https://goenbako.com 【私のご縁箱ページ】 https://goenbako.com/outputky スマホ・PC対応(※若干スマホ寄りのレイアウトです) サービスを作ったきっかけ 私は2021年の4月から10月初旬まで、オンラインのプログラミングスクール(RUNTEQ)で受講していました。 そこで色んな方と交流を深めさせてもらって刺激を受けたり、勉強会で新たな気付きを得た中で、 『そんな人達に普段は伝えることのないような話や感謝の気持ちを伝えたい!』 『一緒に楽しめて、その時の大事な気持ちを忘れないで残しておけるようなサービスを作ろう!!』 という想いでサービスを構想していきました。 発想のタネ?自分のmixiアカウントの紹介文を見たのがきっかけです。 mixiの紹介文は『 プロフィールに自分を紹介する書き込みをしてもらうことが出来る機能 』です。 ( いわゆる他己紹介 ) 現在、自分の周りは誰もINしていませんが紹介文一覧の不思議なアルバムのような空間がとても好きです。 Twitterの検索で 『mixi 紹介文』とワードサーチしたところ、同じように感じている方や、そういったサービスを求めている方が実際に多くいること知り、需要があることを確信しました。 Twitterというプラットフォームにおいてこの価値を表現を出来ないかと考えながら開発しました。 サービス概要 ご縁箱はTwitter認証したユーザー同士でファンレターを交換・共有することが出来るSNS型のサービスであり、Twitter上で使えるコミュニケーションツールでもあります。 認証しなくても一部の機能を楽しんでいただけますが、ここではTwitter認証してご縁箱を開設したことを前提とさせていただきます。 使い方 1. 自分のご縁箱ページをTwitterでシェア ツイート機能で共有するかプロフィールにリンクを載せることで、フォロワーさんに自分のページを共有します。 ▼ 例えば、これは私のご縁箱ページです。 https://goenbako.com/outputky Twitterでご縁箱ページを公開するか、このように公開している相手に対してレターを書くことでもSNSのようにご縁箱の中で繋がっていくことが出来ます。 2. レターを書く 交流したいユーザー、または応援している・気になるユーザーにレターを書くことが出来ます。 話題を自由に選ぶことができ、全て埋める必要もなく、入力した話題だけが送ったレターとして反映されます。 ただ漠然と 『 レターを書いてください! 』 と言われても書きづらくなる問題と 関係性問わず、誰でも負担少なく書けるように..という考えから、テーマを厳選しています。 ちなみに『ホーム』 ▶ 『送ったレター』から後で編集や削除も出来ます。 3. 受け取ったレターのシェア ( デザインOGP ) ※任意 受け取ったレターの項目を一つ選んでシェアすることが出来ます。 ここでOGP画像の生成を行っているため、遷移するまでに少しインターバルが発生します。 受け取ったレターがデザインされたOGP画像となって反映されます。 Twitterでも気持ちの共有やリアクションをして話題を広げたり楽しむことが出来ます。 その他の機能紹介 ❏ レターの詳細 Twitter上でシェアされたツイートからアクセスすると ログインしていなくてもレターの全文を見たり、両者のユーザーページにも飛ぶことが出来ます。 シェアされたレターを通じてさらに第三者がファンレターを書くきっかけに少しでもなればいいなと思い、URL画像デザインとレター詳細ページのレイアウトも拘りました(^^) ❏ ランダム訪問機能 ユーザーのページをワンタップでランダムにアクセスします。 新規のユーザーでも、ご縁箱の使われ方や世界観を感覚的に伝える目的と、 何か少し遊びを入れたいと思い実装しました。 単にユーザーのレコードをランダムに取得して画面遷移しているだけですが、意外と好評な機能です。 ❏ ユーザー検索機能 & 開設リクエスト機能 ユーザー検索機能はTwitterのIDにマッチするユーザーページに遷移します。 検索成功時の挙動(GIF画像) ▼ 検索失敗時は開設リクエストの案内を表示 ユーザー検索でユーザーが存在しなかった場合、 『 見つかりませんでした。 』とただ表示されるだけだと、少し寂しくありませんか? (はい! 寂しいです!!) 検索ヒットしなかったユーザーにご縁箱をリプライで招待する機能を用意し、案内表示することにしました。 メール通知機能 ・ 設定ページ リリース後に要望/フィードバックを頂き、以下の機能を実装いたしました。 RailsのActionMailerで、レターを受け取った場合に登録したアドレスへメールが送信 Twitterのプロフィール情報を再取得 実装について 使用技術 Ruby 2.7.4 Rails 6.0.4.1 JavaScript Vue.js 2.6.14 Gem sorcery carrierwave meta-tags twitter rubocop ライブラリ vuetify vue-router vuex vuex-persistedstate axios js-cookie vee-validate vue-gtag eslint その他 TwitterAPI heroku Amazon S3 ER図 細かめなこだわり ▼ 初心者ユーザーのみログイン時に、一度だけ使い方カードが表示される。 使い方がわかりづらいサービスのため、私のような感覚派かつ受動的なユーザーでも少しでも馴染みやすいように意識しました。 「受け取ったレターが0件の場合」という条件式と、Vuexで「1度表示したかどうか」のステートを保持させることで実現しています。 ハッシュタグを有効活用する 少しでもTwitterで認知していただきやすくするためにハッシュタグを一意でわかりやすいものにし、タグ一覧を見るだけでサービスの概要が掴めるようにしました。 Twitter単体で宣伝する際にも活用出来るものですので、最初にどう表現するかという部分まで慎重に決めました。 その他 全体的なレイアウト、デザインカラーやフォントで世界観を統一し、合わせて細かく素材を配置しました。 少しでもパフォーマンスの低下を抑えたかったため、Adobe Illustratorを使ってSVGのデザイン画像を自作しています。 デザインに関しては完全に素人でしたが、やってみたいと思い、試行錯誤を重ねつつ挑戦することにしました。 ログイン状態の保持Twitterを行き来するサービスですのでvuex-persistedstateでログイン状態を保持するようにしました。 ログインしていなくてもレターを書くボタンを配置ログインしていない場合はそもそもボタンを表示させないという実装が一番容易ではありましたが、導線を用意しておかないのは勿体ないと思い、ログインガイダンスを用意しました。 実装の意図『登録する』『レターを書く』という明確な意志を持ったユーザーであれば別かもしれませんが、 ボタンすらない場合、興味本位で見に来てくれた新規ユーザーは本当に見るだけになってしまうのでは? 無意識でもファンレターを書くボタンを認識してもらうことには意味があると考え、このような設計にしました。 苦労した点 受け取ったレターをシェアする機能 デザインしたSVG画像をレターの内容をVueで反映させ、画像化した上でOGPに動的に反映させるという一連の処理の実装です。 この部分の実装です (Gif画像)画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 SVG要素の扱い方、Canvas、フォントの適用やCSSの適用 OGP画像の仕組みやbase64画像をアップロードする方法 PC、スマホ、開発、本番 とそれぞれの環境下で所々動かないケース など、検証や問題の切り分けと根本的な知識の土台が必要となり、かなりの苦戦と時間を要しました。 具体的な処理の流れは以下のようになりました。 SVGの見えない部分の処理本番ではhiddenにしていますが2番までの処理は以下の画像のようになっています。 最初から表示してあるのが用意しておいたSVGのデザインフレームで、上に表示される画像がbase64画像化したものです。 画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 色々な記事を参考にしましたが、情報の掴み方が難しく、 一番の解決策はリファレンスなどを読み、土台となる知識を固めて挑むことでした。 初めて扱うオブジェクトや概念が多く、時間的な焦りや、厳しいと思うことが多々ありましたが、しっかり把握しながら実装すると着実に進めていけたため、とても楽しかったです。 終わりに 最後まで読んでいただき、ありがとうございました。 本記事もURL追記など改良していけたらと思いますので興味がある、良かったと思う方は是非ストックしていただいて、ご要望や感想などもコメントやTwitterで共有して頂けますと励みになります(^^) 良ければフォロワーさんに紹介していただいて、楽しんでください♪ https://goenbako.com