Highlight.js のサイズを抑える

Nuxt.js
公開日:
更新日:

単に highlight.js 全体を require してしまうと、自分が記事で扱わないであろう言語のハイライト用コードが大量にバンドルされてしまい、JS ファイルが肥大化するだけでなく、言語の自動検出機能の精度も下がります。

必要な言語のみ処理させるには、highlight.js のコアを require して、使う言語を一つ一つ登録します。

const hljs = require('highlight.js/lib/core')
hljs.registerLanguage('xml'require('highlight.js/lib/languages/xml'))
hljs.registerLanguage('scss'require('highlight.js/lib/languages/scss'))
hljs.registerLanguage('javascript'require('highlight.js/lib/languages/javascript'))
hljs.registerLanguage('shell'require('highlight.js/lib/languages/shell'))
hljs.registerLanguage('bash'require('highlight.js/lib/languages/bash'))
hljs.registerLanguage('ini'require('highlight.js/lib/languages/ini'))

subLanguage に注意

登録する言語ファイルには、subLanguage が指定されていることがあります。例えば、シェル用の highlight.js/lib/languages/shell.js を見ると、subLanguage として bash が指定されています。

shell.js は文頭の「$」「#」などに色を付けるだけで、その他の部分は bash.js に丸投げしているので、bash も登録しておく必要があります。

$ #ここから .bash クラスが付いているので bash 扱い

別名の登録

SCSS 用の scss.js は subLanguage を指定しておらず、単体で CSS と SCSS のハイライトができます。

しかし、HTML, XML 用の xml.js では、subLanguage として css を指定しているため、以下のようなコードのハイライトが上手くいきません。

<style>
/* (別名を登録済みなのでハイライトされてるけど) */
/* ここから */
.foo {
  background-color: black;
  color: white;
}
/* ここまでハイライトされない*/
</style>

そこで、hljs.registerAliases() を使って、scss の別名として css を登録します。第一引数は文字列の配列にすることもできます。

hljs.registerAliases('css', { languageName'scss' })

これで、HTML コード内の CSS もハイライトされるようになります。

css.jsscss.js は 430 行目ぐらいまで全く同じ内容で、擬似セレクタや CSS プロパティなどのリストが定義されています。css.js のバンドルを避けることで、6.6 KB ほど削減することができました。

そもそもバンドルしない

Jamstack なブログでは JavaScript の実行結果をそのまま配信できるので、記事データを取得して Highlight.js で処理したものを payload に渡せば、そもそも Highlight.js をバンドルしなくて済みます。

処理内容を統一するため、モジュール化します。

// assets/js/filterPost.js
const hljs = require('highlight.js/lib/core')
hljs.registerLanguage('xml'require('highlight.js/lib/languages/xml'))
hljs.registerLanguage('scss'require('highlight.js/lib/languages/scss'))
hljs.registerLanguage('javascript'require('highlight.js/lib/languages/javascript'))
hljs.registerLanguage('shell'require('highlight.js/lib/languages/shell'))
hljs.registerLanguage('bash'require('highlight.js/lib/languages/bash'))
hljs.registerLanguage('ini'require('highlight.js/lib/languages/ini'))
hljs.registerAliases('css', { languageName'scss' })

const { decodeHTML } = require('entities')

exports.filterPost = post => {
  post.content = post.content.replace(/<pre><code>(.+?)<\/code><\/pre>/gims, (match, p1) => {
    const { language, value } = hljs.highlightAuto(decodeHTML(p1))
    const langClass = language ? ` lang-${language}` : ''
    return `<pre><code class="hljs${langClass}">${value}</code></pre>`
  })
}

記事用テンプレートでは、開発時のみこのスクリプトを require します。generate 時は payload からデータを受け取ります。

<!-- pages/_postId/index.vue -->
<template>
  ...
</template>
<script>
let asyncData = ({ payload }) => payload

if (process.env.NODE_ENV !== 'production') {
  const { filterPost } = require('~/assets/js/filterPost')

  asyncData = async ({ $api, params }) => {
    const { 'data': post } = await $api.get(`posts/${params.postId}`)
    filterPost(post)
    return { post }
  }
}

export default {
  scrollToToptrue,
  asyncData,
  head() {
    return {
      titlethis.post.title,
    }
  },
}
</script>

記事のプレビューページでは、mounted で記事データを取ってきて、その場でハイライトすることになるので、Highlight.js をバンドルする必要があります。

データには created でも触れるのですが、開発時にサーバーとクライアントで描画内容が違うと怒られがちなので、mounted での取得で良いと思います。

<script>
const { filterPost } = require('~/assets/js/filterPost')

export default {
  scrollToToptrue,
  data() {
    return {
      post: {},
      loadingtrue,
    }
  },
  mounted() {
    const { id, draftKey } = this.$route.query

    this.$axios.get('/api/preview', { params: { id, draftKey }})
    .then(('data': post }) => {
      filterPost(post)
      this.post = post
      this.loading = false
    })
    .catch(({ response }) => {
      this.$nuxt.error({
        statusCode: response.status,
        message: response.statusText,
      })
    })
  },
  head() {
    return {
      titlethis.post.title,
    }
  },
}
</script>

Nuxt.js ではページごとにスクリプトが分割されるので、preview ページにアクセスしない限り、Highlight.js 入りのスクリプトがダウンロードされることはありません。実質バンドルしてないみたいな感じになります。