もっと速く!Webサイト高速化のためのwebpack活用入門

2017/09/13

James Hibbard

135

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

webpackを使ってアセットをバンドルすることで、HTTPリクエストの回数を削減し、ファイルサイズを縮小できます。Webサイトをもっと快適にするためのチュートリアルです。

webpackが大人気です。GitHubのスター数は30,000を超え、ReactやAngularといったJavaScript界の有名プレイヤーもwebpackを歓迎しています。

webpackの本質はバンドラーであり、webpackを使えばどんなリソースやアセットでもバンドルできます。大規模なプロジェクト以外でも活用できます。

webpackをインストールして設定する方法を説明します。いくつかのアセットを持つシンプルな静的サイトのための圧縮バンドルをwebpackで作成します。

webpackを使う理由

主な理由は、サーバーへのHTTPリクエストの回数を減らすためです。Webページが大きくなると、jQuery、フォント、プラグイン、各種スタイルシート、自作のJavaScriptなどをインクルードします。アセット1つ1つのためにネットワークリクエストを出すと、リクエスト回数が膨張しWebページが重くなります。上記のアセットすべてを1つのバンドルにまとめることで、問題を解決します。

webpackは、好きな方法でアセットを書くことができ、簡単な方法でコードを圧縮してサイズを削減できます。本記事ではwebpackでES6をES5にトランスパイルする方法を説明しますが、webpackなら最新の構文(ただしこの構文はまだ完全にはサポートされていません)で書いたJavaScriptをほぼすべてのブラウザーが実行可能なES5として提供できるのです。

本記事でwebpackの概要と、使い方、プロジェクトに合うかを理解できます。プロジェクトで使うかはみなさん次第です。

準備

コンピューターにNodeとnpmをインストールします。Nodeを持っていない場合は、NodeのWebサイトからダウンロードするか、バージョンマネージャーを利用してダウンロード、インストールしてください。複数バージョンのNodeを切り替えられることや、Nodeパッケージを管理者権限でインストールしなければならない問題を引き起こす一連のパーミッションエラーが発生しないため、後者の方法がオススメです。

作業にはスケルトンプロジェクトが必要です。作成したものがここにあります。各自のマシンで実行するには、GitHubのプロジェクトをクローンして依存オブジェクトをインストールしてください。

git clone https://github.com/sitepoint-editors/webpack-static-site-example
cd webpack-static-site-example
npm i

Webサイトで使う2つのプラグイン、Slick SliderLightbox2がプロジェクトのルートであるnode_modulesフォルダーにインストールされます。

インストール後、ブラウザーでindex.htmlをブラウザーにアクセスしてください。次の画面が表示されるはずです。

Our static site

プロジェクトにwebpackを導入する

webpackをインストールします。インストールするには以下のコマンドを実行します。

npm i webpack --save-dev

webpackがインストールされ、devDependencyとしてpackage.jsonファイルに追加されます。

"devDependencies": {
  "webpack": "^3.2.0"
}

バンドルしたJavaScriptを格納するためのdistフォルダーを作成します。

mkdir dist

コマンドラインでwebpackを実行して、正しくセットアップされたことを確認します。

./node_modules/webpack/bin/webpack.js ./src/js/main.js ./dist/bundle.js

src/js/main.jsの内容をdist/bundle.jsにバンドルするようwebpackに指示しています。インストールが成功していればコマンドラインに以下のアウトプットが表示されます。

Hash: 1856e2c19ecd9b2d9026
Version: webpack 3.2.0
Time: 50ms
    Asset     Size  Chunks             Chunk Names
bundle.js  2.67 kB       0  [emitted]  main
   [0] ./src/js/main.js 192 bytes {0} [built]

webpackがdistフォルダーにbundle.jsファイルを作成します。任意のテキストエディターでファイルを開くと、中に一連のボイラープレートがあり、一番下にmain.jsの内容があります。

セットアップを自動化する

webpackを実行する度、上記のコマンドをターミナルに入力するのは面倒です。そこで、npmスクリプトを作成して実行します。

package.jsonscriptsプロパティを変更します。

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack ./src/js/main.js ./dist/bundle.js"
},

スクリプトから実行する場合、npmがnode_modulesフォルダーからwebpackモジュールを自動的に探し出します。webpackモジュールをフルパスで指定する必要はありません。npm run buildを実行すると、同じことができます。

webpackの設定ファイルを作成する

バンドルするファイルのパスと、アウトプットファイルのパスを引数としてwebpackに渡します。これらを設定ファイル内で指定するように変更します。あとでローダーを使うときの作業が簡単になります。

プロジェクトのルートにwebpack.config.jsファイルを作成します。

touch webpack.config.js

以下のコードを追加します。

module.exports = {
  entry: './src/js/main.js',
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  }
}

npmスクリプトを変更します。

"scripts": {
  ...
  "build": "webpack"
},

webpack.config.jsは、エントリポイントとバンドルのアウトプット先を設定オブジェクトのプロパティとして指定し、設定オブジェクトをエクスポートします。実行すると前と同じように動きます。

バンドルをインクルードする

webpackを使ってバンドルが生成できるので、次はバンドルをインクルードします。その前に、別のエントリポイントを作ってmain.jsを読み込みます。src/jsディレクトリにファイルapp.jsを作ります。

touch src/js/app.js

app.jsに以下を追加します。

require('./main.js');

webpackの設定を変更します。

entry: './src/js/app.js',

再びnpm run buildを実行してバンドルを作り直します。前と同じように動くはずです。

index.htmlを見てください。JavaScriptは多くありません。ファイルの最後にjQueryとmain.jsファイルがインクルードされています。main.jsRead more…リンクをクリックしたときに情報を表示する役割を担っています。

index.htmlを編集してmain.jsの代わりにバンドルをインクルードします。ファイルの末尾に以下のコードがあるはずです。

    <script src="./node_modules/jquery/dist/jquery.min.js"></script>
    <script src="./src/js/main.js"></script>
  </body>
</html>

Change this to:

以下のコードに変更します。

    <script src="./node_modules/jquery/dist/jquery.min.js"></script>
    <script src="./dist/bundle.js"></script>
  </body>
</html>

ブラウザーでページを再読み込みしてRead more…リンクが変わらずに動作することを確認してください。

jQueryをバンドルする

Webページが実行するHTTPリクエストの回数を減らすためjQueryをバンドルに追加します。app.jsファイルを変更します。

window.$ = require('jquery');
require('./main.js');

jQueryを読み込んでいますが、npmでインストールしたためフルパスでインクルードする必要はありません。グローバルオブジェクトであるwindowにおなじみの$エイリアスを追加することで、ほかのスクリプトからも利用できます。

index.htmlからjQueryスクリプトタグを削除します。

    <script src="./dist/bundle.js"></script>
  </body>
</html>

npm run buildを再度実行し、ブラウザーでページを再読み込みしてRead more…リンクが変わらず動作するか確認します。

CSSをバンドルする

index.htmlでネットワークリクエストを発行するはCSSだけです。ページの先頭でmain.cssをインクルードし、ほかの4つのCSSファイルをインポートします。

webpackの標準設定ではJavaScriptだけ扱えますが、ローダーを使えばCSSもバンドルできます。webpackのドキュメントに以下の説明があります。

ローダーはモジュールのソースコードに適用される変換処理であり、ファイルをインポートするときや「ロード」するときにプリプロセシングできます。ローダーはほかのビルドツールで言う「タスク」のようなもので、フロントエンドのビルド作業を処理するための強力な手段を提供します。ローダーはファイルを別の言語(TypeScriptなど)からJavaScriptへ、またはData URLを用いたインラインイメージへ変換します。ローダーを使えばCSSファイルをJavaScriptモジュールから直接インポートできます。

app.jsを変更します。

// CSS
require('../css/main.css');

// JavaScript
window.jQuery = window.$ = require('jquery');
require('./main.js');

webpack.config.jsを変更し、.cssで終わるファイルを見つけたときに実行するローダーを指定します。

module.exports = {
  ...
  module: {
    loaders: [
      {
        test: /\.css$/,
        loaders: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}

css-loaderstyle-loader2つのローダーを指定しました。css-loaderはCSSをJavaScriptモジュールに変換し、style-loaderはJavaScriptモジュールからエクスポートしたCSSをプログラム実行時に<style>タグに注入します。これらをインストールしましょう。

npm i --save-dev css-loader style-loader

npm run buildでwebpackを再び実行します。

Hash: 3699b503299ce447108d
Version: webpack 3.2.0
Time: 409ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  289 kB       0  [emitted]  [big]  main
   [1] ./src/js/app.js 97 bytes {0} [built]
   [2] ./src/css/main.css 1.01 kB {0} [built]
   [3] ./node_modules/css-loader!./src/css/main.css 494 bytes {0} [built]
   [4] ./node_modules/css-loader!./src/css/fonts.css 353 bytes {0} [built]
   [5] ./src/fonts/open-sans/OpenSans-ExtraBold.ttf 271 bytes {0} [built] [failed] [1 error]
   [6] ./node_modules/css-loader!./src/css/layout.css 219 bytes {0} [built]
   [7] ./node_modules/css-loader!./src/css/styles.css 1 kB {0} [built]
   [8] ./node_modules/css-loader!./src/css/responsive.css 359 bytes {0} [built]
  [12] ./src/js/main.js 192 bytes {0} [built]
    + 4 hidden modules

ERROR in ./src/fonts/open-sans/OpenSans-ExtraBold.ttf
Module parse failed: /home/jim/Desktop/webpack-static-site-example/src/fonts/open-sans/OpenSans-ExtraBold.ttf Unexpected character '' (1:0)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)
 @ ./node_modules/css-loader!./src/css/fonts.css 6:131-183
 @ ./node_modules/css-loader!./src/css/main.css
 @ ./src/css/main.css
 @ ./src/js/app.js

なにが起きたのでしょうか? アウトプットを見ると、src/css/fonts.cssでエラーが発生しています。ファイルの5行目でインクルードしたカスタムフォント(src/fonts/open-sans/OpenSans-ExtraBold.ttf)の処理をwebpackに指示していないのが理由です。

ローダーをもう1つ使えば解決します。url-loaderでフォントや画像といったアセットをData URLに変換し、バンドルに追加します。

module.exports = {
  ...
  module: {
    loaders: [
      {
        test: /\.css$/,
        loaders: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.ttf$/,
        loaders: [
          'url-loader'
        ]
      }
    ]
  }
}

インストールします。

npm install url-loader --save-dev

これでビルドを実行できます。CSSのインクルードを削除し、バンドルを作り直し、ページを再読み込みしてテストします。

サードパーティ製ライブラリーをバンドルする

次はphotos.htmlを説明します。Slick SliderLightbox2と、2つのライブラリーを使っているため、コード量が多めです。どちらもjQueryに依存しています。これまで学んだ手法の応用でライブラリーをバンドルできます。

app.jsを変更します。

// CSS
require('slick-carousel/slick/slick.css')
require('slick-carousel/slick/slick-theme.css')
require('lightbox2/dist/css/lightbox.min.css')
require('../css/main.css');

// JS
window.$ = require('jquery');
window.slick = require('slick-carousel');
window.lightbox = require('lightbox2');
require('./main.js')

ドキュメントの先頭のCSSのインクルードと末尾のスクリプトのインクルードを削除します。削除後のドキュメントは以下の通りです。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>I Can Haz Cheeseburger?</title>
  </head>
  <body>
    ...

    <script src="dist/bundle.js"></script>
    <script>
      $('.slick-slider').slick({
        dots: true,
        arrows: false,
        infinite: true,
        speed: 500,
        fade: true,
        cssEase: 'linear'
      });
    </script>
  </body>
</html>

Slickを初期化するコードもバンドルにインクルードできますが、この処理は当該ページでしか使わないので、ページ上に残します。

webpackを実行してなにが起きるか確認します。

ERROR in ./node_modules/slick-carousel/slick/ajax-loader.gif
Module parse failed: /home/jim/Desktop/webpack-static-site-example/node_modules/slick-carousel/slick/ajax-loader.gif Unexpected character '' (1:7)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)
 @ ./node_modules/css-loader!./node_modules/slick-carousel/slick/slick-theme.css 6:119-147
 @ ./node_modules/slick-carousel/slick/slick-theme.css
 @ ./src/js/app.js

ERROR in ./node_modules/slick-carousel/slick/fonts/slick.eot
Module parse failed: /home/jim/Desktop/webpack-static-site-example/node_modules/slick-carousel/slick/fonts/slick.eot Unexpected character '' (1:0)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)
 @ ./node_modules/css-loader!./node_modules/slick-carousel/slick/slick-theme.css 6:309-337 6:362-390
 @ ./node_modules/slick-carousel/slick/slick-theme.css
 @ ./src/js/app.js

エラーです! 今回はslick-theme.cssファイルに問題があるようです。slick-theme.cssがgifフォーマットの画像を参照していますが、webpackはgifの扱い方を知りません。そのため、なす術なく停止したのです。どうすれば良いか分かりますね。

webpack.config.jsに以下を追加します。

{
  test: /\.(svg|gif|png|eot|woff|ttf)$/,
  loaders: [
    'url-loader'
  ]
}

そのほかのファイル形式にも一致するように正規表現を変更しました。すべてSlickやLightbox2で必要なファイル形式です。webpackを再度実行し、エラーなく完了することを確認します。ページを再読み込みして、問題がないことを確認してください。

最後の仕上げ

ほぼ完成ですが、改善できる点がいくつかあります。

FOUCに対処する

ページ読み込み時のFOCU(flash of unstyled content)<script>タグがレンダリングをブロックする性質を利用して、<script>のインクルードをファイルの先頭に移動させて対処します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>I Can Haz Cheeseburger?</title>
    <script src="dist/bundle.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

バンドルを圧縮する

フラグ(productionの略)をつけてwebpackを実行するとバンドルを圧縮できます。

package.jsonを変更します。

"scripts": {
  ...
  "build": "webpack -p"
},

webpack.config.jsを変更します。

module.exports = {
  entry: './src/js/app.js',
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.min.js'
  },
  module: {
    ...
  }
}

index.htmlphotos.htmlを変更します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>I Can Haz Cheeseburger?</title>
    <script src="./dist/bundle.min.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

バンドルのサイズが480kbまで減りました。いくつかのCSS、JavaScript、その他のアセットからなるバンドルにしては重いように思えますが、222kbはフォントです。

ES6をES5にトランスパイルする

babel-loaderをインストールしてwebpackを実行すると、ES6のJavaScriptファイルがES5にトランスパイルします。

npm install babel-core babel-loader babel-preset-es2015 --save-dev

webpack.config.jsを変更します。

{
  test: /\.js$/,
  loader: 'babel-loader?presets[]=es2015'
},

ES6同様、JavaScriptにコンパイルされるほぼすべての言語を利用できます。

最後に

本記事では、webpackを使い、シンプルな静的サイトのバンドルを作る方法を説明しました。HTTPリクエストの回数が減り、Webサイトがよりサクサク動きます。また、webpackにバンドルを圧縮させてファイルサイズを大きく削減する方法や、babel-loaderでES6をトランスパイルする方法も説明しました。

この使い方がすべての人のニーズを満たせるわけではありませんが、webpackがどういうものかと、使い方の洞察を得られたかと思います。webpackをさらに知りたい場合はまだ使ってないの? Web開発を超効率化するwebpack 2はもはや必須ツールだを読んでください。

(原文:How to Bundle a Simple Static Site Using webpack

[翻訳:薮田佳佑/編集:Livit

Copyright © 2017, James Hibbard All Rights Reserved.

James Hibbard

James Hibbard

太陽の光がさんさんと降り注ぐ北ドイツに住むWeb開発者です。JavaScriptやRubyのコーディングを楽しみ、しばしばSitePointのJavaScriptフォーラムに現れることもあります。コーディングをしていないときはランニングに熱中しています。

Loading...