lh_score.png
Dec 12, 2018

阿部寛を超えるための技術: はてなブログからNuxtに移行した話

この記事はFOLIO Advent Calendar 2018 12日目の記事です。


これまで2年ほどお世話になったはてなブログから乗り換えて、この度個人ブログを新調しました。

https://blog.andoshin11.me/

Nuxt + Contentfulという構成のPWA対応アプリケーションをGAEにホストしています。

プロダクションで回遊するとこんな感じ。

Studio Andy

我ながらヌルサクなWebサイトになったかと思います。

せっかくモダンな設計に作り直してPWA対応も行うということで、

「Lighthouse Performance Scoreで阿部寛のHPを超える!」

という事を一つのベンチマークにパフォーマンスチューニングに挑みました。

今回はその結果と、開発時に行った内容のサマリーをご紹介します。

また、本ブログのソースコードはPublicな状態で公開しています。要望・問題等ありましたら気軽にIssueやPRをお待ちしています。

Embedded content: https://github.com/andoshin11/studio-andy

Lighthouse Score

まずはこれを見てくれ!!

阿部寛さんのHP

スクリーンショット 2018-12-11 1.29.51

どちらもPerformance Scoreは100点!そしてFirst Contentful Paintでは僕のサイトの方が1/2という結果に!

勝った!!阿部寛に勝利したぞ!!!!!!

実測値

せっかくなので本ブログと他のいくつかのウェブサイトもLighthouseで比較してみましょう。計測環境は以下の通りです。

- 端末 MacBook Pro (Retina, 15-inch, Mid 2015)
- プロセッサ 2.5 GHz Intel Core i7
- メモリ 16 GB 1600 MHz DDR3
- Chrome 70.0.3538.110(Official Build) (64 ビット) Incognito Mode

まずはキャッシュをオフにした状態の計測から

対象サイト Performance PWA A11y Best Practice SEO FCP FMP
本サイト 99 100 100 100 100 980ms 980ms
阿部寛HP 100 31 18 80 67 900ms 900ms
日経電子版 27 19 58 73 82 3240ms 3310ms
dev.to 87 96 86 93 91 940ms 990ms
旧サイト(はてなブログ) 26 12 61 60 91 1560ms 1640ms

次にキャッシュをオンにした状態

対象サイト Performance PWA A11y Best Practice SEO FCP FMP
本サイト 100 100 100 100 100 10ms 30ms
阿部寛HP 100 31 18 80 67 20ms 20ms
日経電子版 79 46 58 73 82 1960ms 1980ms
dev.to 97 96 86 93 91 780ms 850ms
旧サイト(はてなブログ) 34 12 61 53 91 1530ms 1530ms

旧サイト(はてなブログ)からのパフォーマンスの伸びが顕著ですね。

Performanceは言わずもがなですが、はてなブログでPWA対応がほとんどされていないのが気になりました。Service Workerのような標準化しつつある仕様に追従できていないのは痛いところ。

Nuxt + Contentfulでやっていき

今回の実装にあたってはフレームワークに依存せずSPAを作成することも検討しましたが、SEOなどを考えるとSSRの仕組みが必須です。またSSRもスクラッチで組もうとすると中々ハードルが高く(Storeのハイドレーション、Render hooksのAPI設計等)、今回は業務でも利用した事のあるNuxt.jsを利用しました。

Embedded content: https://nuxtjs.org/

詳細は後述しますが、Nuxt communityの提供しているModule群がものすごく使い心地が良く、おかげで爆速で開発を進めることができました。

また、Clean Architecture大好きマンとしてフレームワークにロックインされるリスクも考慮したのですが、Nuxt独自の仕組みもそこまで多くはなく(Routerの設定くらい?)将来別のフレームワークにコードベースの大部分を移植できそうだという判断で採用に至りました。

Embedded content: https://www.contentful.com/

ブログコンテンツをホストするヘッドレスCMSには最近人気上昇中のContentfulを利用しています。

N to Nのリファレンスタグを実装する際などは少し難がありそうですが、全文検索クエリを発行してもレスポンスが非常に良く、APIも必要十分なものが提供されていたためお試し的に採用。

Content Modelのschemaをgenericsで渡せるようになっており、TypeScriptとの相性も良さそうです。

開発パート

TypeScript開発環境の設定

昨日公開したこちらの記事にて。

Embedded content: https://blog.andoshin11.me/posts/nuxt-with-typescript

アーキテクチャ

今回もClean Architecutre 風味な設計を採用。透過性の高い Use Caseレイヤーを中心に、各種Adapterを設計しています。

ステート管理にはVuexを利用していますが、 mutationsを通した各プロパティのCRUD以外は行わないObservable Dumb State Boxとして使うのが僕は好きです。 actionsも使いません。

CI/CD

GitHub上の masterブランチに変更が加えられたタイミングでCircle CI Workflowをトリガーし、CI上からGAEにデプロイを行っています。

サービスアカウントの作成方法やインスタンスの設定方法はネットにたくさん資料があるので割愛。

気になる方は .circleci/config.yamlapp.yamlあたりを参考にどうぞ。

SentryへのSourcemapのアップロードもこちらで行っています。

Performance対策

ここからが今回の本題であるパフォーマンスチューニング編です。

WebPの利用

旧ブログから移行した記事はともかく、新しい記事内や一覧ページでは要所にWebPを利用しています。

WebPは画像ファイルの圧縮効率がPNGよりも優れているだけでなく、デコード時のパフォーマンスにも分があるため、最新のブラウザで高速にWebページを表示する際には対応必須と言えるフォーマットでしょう。

圧縮作業は $ brew install webpでコマンドをインストールした後にCLIで

$ cwebp hoge.png -o hoge.webp -q 50

を実行して手動で行ってます(効率化したい)。圧縮時に指定するクオリティは画像ごとにチューニングの余地がありそうですが、雑に50付近を指定。

ブラウザでの描画時は <picture />タグで画像領域をラップして、WebP非対応ブラウザへのフォールバックをきちんと提供しましょう。

<picture>
  <source srcset="hoge.webp" class="img" type="image/webp">
  <img src="hoge.png" alt="hogehoge" class="img">
</picture>

アセットコンプレッション

1/1000秒でも速くエンドユーザーにコンテンツを届けるためには、サーバーから配信するアセットのサイズを圧縮する事が重要です。上記のようにImage Assetは手動でcompressionを行いましたが、その他のスクリプトファイルについては基本的にはNuxtが自動でgzip化してくれます。

詳しくは公式のドキュメントを参照してください。

また、 brotliのようなアルゴリズムで圧縮することも可能なので、興味がある方はこちらの記事に目を通してみてください。

Embedded content: https://blog.lichter.io/posts/nuxtjs-on-brotli

クリティカルレンダリングパス

ブラウザがレンダーツリーの構築を完了してレンダリング処理を開始するためには、メインリソース(html)だけでなくサブリソース(CSS, JS)の読み込みも完了させなければいけません。

ひと昔前まではサイトのスタイルシート(CSS)は独自ファイルとして分割して単体でCDNでホストする手法が主流でしたが、2018年ではページの描画に必要なCSSのみを始めからHTML Headにインラインで埋め込む方式が主流です。

これによりブラウザがHTMLをパースしてDOMツリーを構築する際に、同時にCSSOM(CSS Object Model)の構築を完了することができ、ブロックを発生させることなくレンダリング処理が実行できます。

Inline CSSについてはNuxtがデフォルトで処理を行ってくれるので、ユーザーが行うことは特にありません。

First Contentful Paint

index.htmlをプレビューすると、サブリソース無しでも有意なレイアウトが描画できている事が分かります。First Contentful Paintというやつですね。

FCPを速くするにはCSSの設計を工夫してやるのがポイントです。例えば画像の縦横サイズや、API通信の結果に依存したスタイリングを行っていると、リソースの読み込みが完了したタイミングで大きくレイアウトがずれる(ガタつく)現象が発生します。 ユーザーの印象としてもよろしく無いため、画像のプレースホルダを利用するなど工夫をしましょう。

Critical Rendering Paths

その他にレンダリングのブロッカーとなるのは _nuxt/*.jsというスクリプト群ですが、上のWaterfallを見ても分かる通りHTTP 2を利用して並列に読み込みが行われており、DOMContentLoadedまでの所要時間も160ms前後と十分高速だと判断しました。

後述しますが、これらのアセットの読み込みにはService Workerのキャッシュ機構をフルに活用しており、その事が高速化に寄与しています。

メモリリーク対策

こちらの動画を参考に、愚直にmemoryのsnapshotと睨めっこしながら余分なHeap Arrocationが発生している部分を潰していきます。

Embedded content: https://www.youtube.com/watch?v=RJRbZdtKmxU

バンドルサイズの削減

NuxtではデフォルトでRoute単位のCode Splittingが適用されますが、今回はコンポーネントレベルのDynamic Importも行っています。Webpack 4を利用しているならVue ComponentのDynamic Importは簡単です。

...
const SomeComponent = () => import('./SomeComponent.vue')

export default Vue.extend({
  components: {
    SomeComponent
  }
...
})

これまで import SomeComponent from './SomeComponent.vue'と書いていた部分を上記のように変更してあげるだけ。

現在のバンドルサイズは $ nuxt build --analyzeで確認しましょう。通常のWebpackプロジェクトと同じく、Chunk単位のバンドルサイズと、内包されるスクリプトの比率が確認できます。こいつと睨めっこしながら、削減可能な部分をLazy Loadしたり、別の軽量ライブラリに置き換えるなどしてあげてください。

スクリーンショット 2018-12-11 2.03.36

VueのLazy LoadパターンについてはVueConf TO 2018でコアチームメンバーであるEduardoが話しているので、時間のある方はそちらもご覧ください。

Embedded content: https://youtu.be/5nr8zLo9hAg

リソースヒント

Resource Hint APIを活用すると、通信のオーバーヘッドを削減したりユーザーが今後必要になるであろうリソースを投機的に先読みすることが可能になります。

dev.to日経電子版の事例でご存知の方も多いかと思いますが、ユーザーが別ページに遷移するためのリンクにマウスをホバーしたタイミングでリソースの取得を開始し、ユーザーがリンクをクリックした瞬間には既に読み込みが完了している = ほぼゼロ遅延でページ遷移を行えるというといった事ができるというやつです。

弊ブログでも同様の仕組みを実装してみました。

Chrome Dev Tools等を開きながら確認していただけると分かりやすいのですが、記事の一覧ページで個別の記事にマウスをホバーするとそのURLをhrefに持った <link />タグがHTML Headに挿入され、 prerender処理が走るのが分かります。

prerender

実際にPrerenderingが動いていることを確認するにはChromeのタスクマネージャー(chrome://net-internals/#prerender)を開いて Active Prerender Pagesに該当のURLが表示されている事を確認してください。

Active Prerender Pages

Prerenderの注意点としては、1度に1ページのPrerenderingしかできないこと、 Prerender対象を頻繁に更新しすぎると「Too many processes」や「Deprecated」で怒られてPrerenderingがキャンセルされてしまうことなどがあります。この辺りもネットにたくさんの実装例が落ちているはずなのでそちらをご参考ください。

ちなみに自分は下記のようなシンプルなロジックを、マウスホバーイベントをフックに読んでいます

// src/util/util.ts
export const prerender = (href: string): void => {
  const elementId = 'prerendering-header'
  const _link = document.getElementById(elementId)

  if (_link) {
    const _href = _link.attributes.getNamedItem('href')
    if (_href && _href.value === href) return

    // remove previous link before creating new one
    _link.parentNode && _link.parentNode.removeChild(_link)
  }

  const link = document.createElement('link')
  link.id = elementId
  link.rel = 'prerender'
  link.href = href
  document.head && document.head.appendChild(link)
}

Resource Hint APIには prerender以外にも様々なプロパティが存在します。

ちなみにNuxtのアーティファクトに対してはデフォルトで preloadが割り当てられているのでこの辺りも高速読み込みの秘訣です。

Resource Hint APIは奥が深いので、是非いろいろなフックで計測してみてください。

エッジキャッシュの利用

メインリソース(index.html)以外の静的アセットは基本的にGAEから利用できるエッジキャッシュに乗せて配信しています。

キャッシュの設定は app.yamlに以下のような設定を書くだけ

# app.yaml
...
handlers:
  - url: /_nuxt
    static_dir: .nuxt/dist/client
    secure: always
  - url: /(.*\.(gif|png|jpg|ico|txt|js|svg))$
    static_files: src/static/\1
    upload: src/static/.*\.(gif|png|jpg|ico|txt|js|svg)$
    secure: always
  - url: /.*
    script: auto
    secure: always
default_expiration: '1d'
...

NuxtのStatic Directoryに登録されているアセットとClient Artifactへのアクセスを24時間キャッシュし、高速でレスポンスを返しています。

これによりユーザー体験が良くなるだけでなく、インスタンス時間の削減にも繋がるので低価格でGAEを運用する事も可能に。

本当はGAEインスタンスに1リクエストもユーザーを到達させたくないのですが、この辺りはまだまだチューニング途中というのが正直なところです。

Service Workerを利用したキャッシュ戦略

みんな大好き、Service Workerのお話です。

まず何はともかくNuxt公式のpwa-moduleを入れておきましょう。

Embedded content: https://github.com/nuxt-community/pwa-module

このModuleを利用すると自動で配信ルートディレクトリにService Workerが配置されるだけでなく、Nuxtのbuild artifactが固有で持つrevision hashに応じたprecache asset urlの登録まで行ってくれます。

すごい!優秀!!

他にも nuxt.config.jsのオプションから manifest.jsonの生成が行えたり、Iconの自動生成、OnesignalによるPush通知設定等もカバーしている優れものです。詳しくは公式のドキュメントを参考に。

上記のModuleを利用すればWorkboxの設定も簡単に行えるのですが、こちらについてはあまりDSLに依存した記述をしたくなかったので外部ファイルに切り分けました。(外部ファイルのマウントはpwa-moduleのオプションから簡単に指定できます)

今回定義したService Worker(Workbox)の設定はこんな感じ

// main-sw.js
workbox.routing.registerRoute(
  new RegExp('^https://cdn.contentful.com/spaces/2p1otbbee5vt/environments/master/'),
  workbox.strategies.staleWhileRevalidate({
    cacheName: 'entry-cache',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200]
      }),
      new workbox.expiration.Plugin({
        maxAgeSeconds: 60 * 60 * 24 * 14 // for 2 weeks
      })
    ]
  })
)

workbox.routing.registerRoute(
  new RegExp('^https://images.ctfassets.net/2p1otbbee5vt/.*.(png|jpg|webp)$'),
  workbox.strategies.cacheFirst({
    cacheName: 'image-cache',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200]
      }),
      new workbox.expiration.Plugin({
        maxAgeSeconds: 60 * 60 * 24 * 14 // for 2 weeks
      })
    ]
  })
)

...

workbox.routing.registerRoute(
  new RegExp('^https://fonts.googleapis.com/'),
  workbox.strategies.cacheFirst({
    cacheName: 'google-fonts-cache',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200]
      }),
      new workbox.expiration.Plugin({
        maxAgeSeconds: 60 * 60 * 24 * 30 // for 1 month
      })
    ]
  })
)

キャッシュ対象となる項目はChromeのNetworkタブで通信を監視しながら細かくリストアップしましたが、重要なものは記事の情報、画像Asset、Google Fontsの3つです。

Strategiesに関しては、更新が頻繁に発生する記事情報を取得するAPIについては staleWhileRevalidateを、その他のものについては基本的に cacheFirstを指定しています。

cacheFirstはその名の通りローカルにCacheがあればそちらを表示し、無ければフォールバックとしてネットワークにリクエストを発行する方式です。ただしこの方式はCacheが有る限りネットワークへの通信が発生せず、Expireするまで恒久的にその値が参照されてしまう点に注意が必要です。

それに対して staleWhileRevalidateはコンテンツを返すまでのフローは cacheFirstと同じですが、ローカルキャッシュを返却した場合もネットワーク通信を必ず行い、その取得結果をキャッシュに注入して次回以降はそちらを返すという特徴があります。

これにより、ユーザーへはキャッシュ上の値を高速で返しつつも、次回以降のレスポンスは非同期で更新されるという上質なUXを提供する事が可能です。

その他にはリクエストの結果が200の時だけキャッシュ化する設定や、コンテンツのExpire期間の設定等を行っています。

オフライン表示

Progressive Web Appをオフラインでも利用できるようにするには、Service Workerによるキャッシュ戦略だけでなく、ネットワークへの依存度で適切なスコープを切ってApp Shellモデルを構築する事が大切です。

これもNuxtの良いところの一つなのですが、Nuxtが提供するLayoutを活用したりRouterのScopeを理解することで出来上がったレイヤードアーキテクチャが、不思議とApp Shellモデルに近しいものになるケースが多いです。 この辺りもNuxtの規約がもたらすメリットと捉えて良いかもしれません。

今回は複雑なログイン機構やユーザー依拠の機能も持たないシンプルなアプリケーションなので、簡単にオフライン対応することができました。(Thanks to PWA module!)

Working offline

オフラインでも動作するナウいブログになりましたね!

SEO対策

Server Side Rendering

最近のGoogle Crawler Botは優秀なのでJS実行後の結果をインデクシングしてくれるらしいですが、Crawlerの実行環境であるChrome Engineのバージョンが古かったり、Google以外のプラットフォーム(Twitter, FB, Slack)でサイトのコンテンツを展開する事を考えると、まだまだSSRは必須です。

Nuxtを利用する最大の利点はこのSSRの行いやすさにあります。下記のように fetch APIを利用すると、リクエストごとに非同期処理を事前に行った上でレンダリングした結果を返してくれて最高です。

// pages/posts/_slug.vue
...
  async fetch({ params, store, $sentry, error }) {
    try {
      const usecase = new FetchPostUseCase({
        postRepository: new PostRepository(store),
        logService: new LogService({ logger: $sentry }),
        contentfulGateway: new ContentfulGateway()
      })
      await usecase.execute(params.slug)
    } catch (e) {
      if (e.type === ErrorType.NOT_FOUND) {
        error({ statusCode: 404, message: e.message })
        return
      }
      throw e
    }
  }
...

Vuex Storeに加えた変更もシリアライズとハイドレーションを自動で行ってくれるため、そのままクライアントサイドで利用可能です。

特定の例外をトリガーにNuxt contextが持つ error()関数を叩いてあげると、自分で定義したエラーページに飛ばすようなハンドリングも簡単に行えます。

Sitemapの自動生成

検索エンジン向けの sitemap.xmlの設定にはNode sitemapのラッパーであるsitemap-moduleを利用しました。

Embedded content: https://github.com/nuxt-community/sitemap-module

async/awaitでクロール対象のルート一覧を渡せるため、ブログで利用している記事一覧取得ロジックをそのまま使いまわせます。StaticなルートについてはNuxt内のRouterから自動解決されるため、手動で登録する必要はありません。

// nuxt.config.js
...
  sitemap: {
    hostname: 'https://blog.andoshin11.me',
    exclude: ['/search'],
    async routes() {
      const contentful = require('contentful')
      const client = contentful.createClient({
        space: process.env.CTF_SPACE_ID,
        accessToken: process.env.CTF_CDA_ACCESS_TOKEN
      })

      const posts = await client.getEntries({
        content_type: 'post'
      })
      return posts.items.map(item => `posts/${item.fields.slug}`)
    }
  },
...

これだけでページ内全ルートのSitemap生成が完了です。クローラーが当該ルートにアクセスすると、上記の関数により自動生成された sitemap.xmlが返却されます。デフォルトのキャッシュ時間は15分です。

meta情報の設定

Nuxtが提供するAPIにより、各Vue component内でheadに情報を渡すことができます。例えば個別の記事ページならこんな具合に。

// containers/Post/index.vue
...
  head() {
    return {
      title: `${this.presenter.post ? this.presenter.post.props.title : ''} | Studio Andy`,
      meta: [{ hid: 'description', name: 'description', content: this.presenter.post ? this.presenter.post.props.summary : '' }]
    }
  },
  computed: {
    ...

トップレベルのPage Containe以外でもheadの情報が定義できてしまうため、競合を避けるためにmetaには hidを指定することをオススメします。

その他の設定

エラー検知

TypeScriptの型チェックや最低限のテストを書く事で限りなく可能性を抑えてはいるものの、JavScriptを利用する以上、実行時エラーの危険性から逃れることはできません。

個人ブログとはいえエラーまみれのサイトをネットの海に放置しておくのはイケてないので、Sentryを利用してログの監視・検知・通知を行っています。

トップレベルミドルウェアを書こうとも思いまいしたが、こちらもNuxt communityからmoduleが提供されていたのでノータイムで採用。

Embedded content: https://github.com/nuxt-community/sentry-module

これでNuxtアプリケーションで発生した全ての例外がSentry Dashboard上に飛んでくるようになります。超簡単。

また実行時エラーではなくても随所で明示的に例外やメッセージをSentry向けに発行したいこともあるので、Sentry JS SDKをラップする LogServiceも用意しました。

import { Logger } from '@/typings/nuxt'

export interface ILogServiceArgs {
  logger?: Logger
}

export enum LogType {
  Error,
  Message
}

type LogPayload = {
      type: LogType.Error
      error: Error
    }
  | {
      type: LogType.Message
      message: string
    }

export default class LogService implements BaseService {
  logger?: Logger

  constructor({ logger }: ILogServiceArgs) {
    this.logger = logger
  }

  async handle(payload: LogPayload) {
    if (!this.logger) return

    if (payload.type === LogType.Error) {
      this.logger.captureException(payload.error)
    } else if (payload.type === LogType.Message) {
      this.logger.captureMessage(payload.message)
    }
    return
  }
}

余談ですが、通知が来るたびに毎度Sentryのダッシュボードを見にいくのもしんどいので、Sentry側の通知は全てOFFにして個人開発用のSlackにログを飛ばしています。

Google Analytics

ルーティングの変更さえ検知できれば十分なので、こちらも公式のanalytics-moduleを利用。

Embedded content: https://github.com/nuxt-community/analytics-module

nuxt.config.jsでGAのIDだけ設定すれば準備OK。GAのアカウント作成も合わせて10分くらいで導入できました。

より細かいイベント検知が行いたい方は、上記Module内で利用されているvue-analyticsのドキュメントを参照してください。

Embedded content: https://matteogabriele.gitbooks.io/vue-analytics/content/

RSS Feed

もはや死にかけとなってしまったRSSですが僕はまだまだFeedlyのヘビーユーザーなので、似たような属性の人たちにはRSSをお届けしたい気持ち。

対応方法はSitemapの時とかなり似ています。公式のfeed-moduleを追加して、 nuxt.config.jsに設定を記述。

Embedded content: https://github.com/nuxt-community/feed-module

// nuxt.config.js
...
  feed: [
    {
      path: '/atom.xml',
      async create(feed) {
        feed.options = {
          title: 'Studio Andy',
          link: 'https://blog.andoshin11.me/feed.xml',
          description: "This is Shin Ando's personal feed!"
        }

        const contentful = require('contentful')
        const client = contentful.createClient({
          space: process.env.CTF_SPACE_ID,
          accessToken: process.env.CTF_CDA_ACCESS_TOKEN
        })

        const posts = await client.getEntries({
          content_type: 'post'
        })

        posts.items.forEach(post => {
          feed.addItem({
            title: post.fields.title,
            id: post.fields.slug,
            link: `https://blog.andoshin11.me/posts/${post.fields.slug}`,
            description: post.fields.summary,
            content: post.fields.summary,
            date: new Date(post.fields.publishedAt),
            image: post.fields.headerImage.fields.file.url
          })
        })

        feed.addCategory('Tech')
        feed.addCategory('Space')

        feed.addContributor({
          name: 'Shin Ando',
          email: 'shinglish11@gmail.com',
          link: 'https://blog.andoshin11.me/'
        })
      },
      cacheTime: 1000 * 60 * 60 * 6, // 6 hours
      type: 'atom1'
    }
  ],
 ...

RSS Typeは atomを指定。6時間おきに更新するようにしています。

Embedly高速化

記事内のリッチコンテンツ(外部サイトリンク、YouTube等)の埋め込みにはEmbedlyというサービスを利用しました。

しかし公式で紹介されていたScriptタグを埋め込んでDOM Loaded後にリッチコンテンツでDOMを上書きする方法があまりにもパフォーマンスが低く、手軽な(そして安価な)SDKも用意されていなかったためちょっとしたハックを行いました。

この場では紹介しませんが、結果としてService WorkerのAPIキャッシュをフル活用できるようになっただけでなく、Vue.jsのVirtual DOMを利用してリッチコンテンツの展開処理を行えるようになったため興味のある方はソースコードを参照してみてください。

今後のTODO

  • ページ全体でWebPを利用する
  • 画像をLazy Loadする
  • アセットのオートコンパイルフローを構築する
  • 記事詳細ページのパフォーマンスとa11y対応状況がヒドい。MarkdownパーサーとEmbed Rich Contentライブラリを自作する
  • Webpage Speed Test等を利用した合成テスト環境の構築
  • サイトトップで不要な情報も大量に読み込まれているため、GraphQLで必要なコンテンツのみを取得するようにする

まとめ・所感

今回は半年ぶりにNuxtを触って、初めてのパフォーマンス・チューニングに挑んでみました。

基本機能の実装やパフォーマンスチューニングのような本質的な作業とは別の部分(Sitemap, Bug Detection, Google Analytics, RSS Feed)についてはガッツリとNuxt公式のModuleを利用させてもらいましたが、どのライブラリも実装自体はとても薄く、あくまでNuxtのインターフェースとサードパーティーライブラリのグルー(接着剤)として機能するものだと理解しました。

Moduleの実装は比較的理解しやすい類のものなので、今後Nuxtを捨てることがあっても大きな負債にはならないはずと期待しています。

ブラウザの仕様は調べれば調べるほど、チューニングの余地が見つかって面白いですね。 PWAやa11yへの対応は比較的すぐに100点が取れるのですが、Performanceの項目を上げるのは本当に大変です。

出来立てのWebサイトが高速なのは当然なので、今のスピードが維持できるよう超速本を片手に今後もパフォーマンスアップを狙って行きます。

明日は@sion_cojpさんの記事です。