こんにちは、虎の穴ラボのはっとりです。
この記事は「虎の穴ラボ Advent Calendar 2022」22日目の記事です。
昨日は辻村さんによる「RailsでMariaDBのCHECK制約を試行してみた話」が投稿されました。
明日は磯江さんによる「Deno向けWebフレームワーク「Fresh」入門」が投稿されます。
こちらもぜひご覧ください。
はじめに
今回は、JavaScriptのMinifyについて、また具体的な方法について紹介します。
Minifyとは
Minify(ミニファイ)とは、インデントや空白を最小限に削ったり、変数・関数名を短いものに置換する等を行い動作を変えずにソースコードの量を少なくすることです。
主にサーバーからクライアントへのデータ転送を削減することが目的です。
※ソースコードの難読化にもなりますが副次的なものだと私は思います。
例えば下記の処理はどちらでも同じ処理になります。
// fizzbuzz.js // このコードは後ほど利用します。 function fizzBuzz(value) { let message = ''; if (value % 3 == 0) { message += 'Fizz'; } if (value % 5 == 0) { message += 'Buzz'; } console.log(value); message && console.log(message); } for(let i = 1; i <= 100; i++){ fizzBuzz(i) }
function f(z){let o="";z%3==0&&(o+="Fizz"),z%5==0&&(o+="Buzz"),console.log(z),o&&console.log(o)}for(let z=1;z<=100;z++)f(z);
前者はファイルにすると268byte、後者は125byteとコード量に倍以上違いがあります。
Minifyの必要性について
ソースコードは読みやすく書くことは大事です。
インデントを揃える・適切に改行を入れる、変数名・関数名は何のためのものか、わかりやすい命名をするなどは基本中の基本です。
人間に読みやすくするとコードの量自体は多くなりがちですが、サーバーサイド処理であればコンパイルしたり、最適化してメモリに展開するなどするためコード量が多いことによるデメリットはほとんどありません。
しかし、クライアントサイドではJavaScript,HTML,CSS等はソースコードをそのままブラウザでダウンロードして処理します。
ソースコードの量が多いとダウンロードにもそれだけ時間がかかります。
高速回線ではそれほど問題になりませんが、データ制限がかかったり、混雑時のモバイル回線等では顕著に影響が出ます。
もちろんWebサーバーなどでgzip圧縮通信に対応させたり、最も伝送する量が多い画像等の遅延読み込みなどMinify以上に有効な対策はあるのでそれらをやった上で、更にMinify対応をするのが良いです。
Minifyするライブラリを直接使ってみる(コマンドライン編)
Minifyはwebpackやvite等のモジュールバンドラを使っていればproduction build時に自動的に行われるので、あまり意識することはないかもしれません。
今回はモジュールバンドラ内でも使えるMinifyライブラリを直接使ってみます。
今回使うのはterserです。
※Minifyライブラリは uglify-js
や uglify-es
などがありましたが、現在は開発が止まっています。
下記のように利用するライブラリをインストールして準備します。
mkdir minify-sample && cd minify-sample # 今回はNode.js18を使いました。 npm init -y npm i -D terser@5
下記のコマンドでMinifyを実行します。 ここでは先程紹介したコードを利用してどんなものが出力されるか確認します。
npx terser fizzbuzz.js -o fizzbuzz.min.js
結果は下記の通りになります。
// fizzbuzz.min.js function fizzBuzz(value){let message="";if(value%3==0){message+="Fizz"}if(value%5==0){message+="Buzz"}console.log(value);if(message){console.log(message)}}for(let i=1;i<=100;i++){fizzBuzz(i)}
無駄な改行やインデントが省略されコードが短くなりました。
更にオプションを加えて実行してみます。
npx terser fizzbuzz.js --mangle -o fizzbuzz.min.js
--mangle
というオプションを追加して実行すると下記のようになります。
// fizzbuzz.min.js function fizzBuzz(z){let o="";if(z%3==0){o+="Fizz"}if(z%5==0){o+="Buzz"}console.log(z);if(o){console.log(o)}}for(let z=1;z<=100;z++){fizzBuzz(z)}
変数が1文字のものに置き換わり、更にコードが短くなりました。
--mangle
以外にも様々なオプションがあります。詳しくはこちら
元コードの書き方や使ったオプションによっては動作が変わってしまうものもあるので注意が必要です。
Minifyするライブラリを直接使ってみる(スクリプト編)
terserはコマンドラインだけでなく、スクリプトで扱うこともできます。
TypeScriptにも対応しています。
# 先程の minify-sample ディレクトリに移動 npm i -D ts-node@10 typescript@4 @types/node@18
build.ts
という名前で下記のスクリプトを追加します。
// build.ts import fs from 'fs'; import path from 'path'; import { minify } from 'terser'; async function main(srcFilePath: string, distFilePath: string) { const src = path.resolve(__dirname, srcFilePath); const dist = path.resolve(__dirname, distFilePath); // ソースコードファイル読み込み const srcCode = fs.readFileSync(src, 'utf-8'); const { code } = await minify(srcCode, { // cliの --mangle に相当 mangle: true, }); if (!code) { throw 'failed!' } // 変換したソースコード書き込み fs.writeFileSync(dist, code, 'utf-8'); } main('./fizzbuzz.js', './fizzbuzz.min.js') .catch((error) => { console.error(error); });
下記のコマンドで実行します。
npx ts-node build.ts
コマンドラインと同じコードが生成されます。
モジュールバンドラから利用する場合
モジュールバンドラ経由で利用する場合、オプションを渡すことができますが、スクリプト編で紹介した内容と同じようなインターフェースになります。
Webpackの場合 https://webpack.js.org/plugins/terser-webpack-plugin/#terseroptions
Viteの場合(ドキュメントにもありますが、Viteで利用する場合は別途terserのインストールも必要です) https://ja.vitejs.dev/config/build-options.html#build-terseroptions
基本的にはモジュールバンドラ標準の設定で十分なMinifyが可能ですが、もう少し進んだMinifyがしたい場合はterserのオプションをいろいろいじってみてください。
余談: 昔のGoogle Analytics埋め込みコードからコードを短くするテクニックを学ぶ
今やGA4対応が叫ばれている中ですが、昔のGoogle Analytics埋め込みコードからJavaScriptのソースコードを短くするテクニックを紹介したいと思います。
下記が Google Analyticsの埋め込みコード(analytics.js版)です
<!-- Google Analytics --> <script> (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); // 省略 </script> <!-- End Google Analytics -->
このJavaScript部分を読みやすく書き換えるとこのようになります。(変数名も変えています。)
(function (window, document, embedTagName, scriptUrl, functionName, newTag, embedTag) { window['GoogleAnalyticsObject'] = functionName; window[functionName] = window[functionName] || function() { (window[functionName].q = window[functionName].q || []).push(arguments) }; window[functionName].l = 1 * new Date(); newTag = document.createElement(embedTagName); embedTag = document.getElementsByTagName(embedTagName)[0]; newTag.async = 1; newTag.src = scriptUrl; embedTag.parentNode.insertBefore(newTag, embedTag); })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
細かい部分を見てみましょう。
(function(i,s,o,g,r,a,m){// 省略 // 省略 })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
windowやdocumentは複数箇所で利用していますが引数で渡すことでwindowやdocumentをiやsなど短い名前で利用することによりコード量を少なくしています。
また、関数の引数は7つあるのに、実際に渡している引数は4つだけになっています。
これは、関数の引数の後ろ3つは関数内で利用する変数のために用意されています。
var c,p,s;
と変数を宣言するより引数でc,o,s
としてしまった方が4文字分少なくできます。
JavaScriptは引数を渡さなかった場合でも特にエラーにならず、関数内では引数にundefinedが入った状態として扱われるのを利用しています。
普通のコードでは読みにくくなるので、使うことは少ないかと思います。
window[functionName] = window[functionName] || function() { (window[functionName].q = window[functionName].q || []).push(arguments) };
オリジナルのコードは見づらいので書き換えた方で解説します。
ga関数を生成している部分ですがga関数がなけれ作るというのを(ほぼ)1ラインでやっています。
これは割と普通のコードでも使う場面はあるかもしれません。
1*new Date()
Unix Timestamp を取得する場合、new Date().getTime()
を使いますが1*new Date()
でも同じ値が取れるのでこれにより8文字節約できます。
まとめ
Minifyについてと、terserによって具体的にMinifyする方法をご紹介させていただきました。
あまり気にすることが少ない内容かもしれませんが、モジュールバンドラでproduction buildした際に動かないなどが発生した場合はMinifyを疑ってオプションをいじってみると良いかもしれません。
P.S.
採用
虎の穴では一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。
yumenosora.co.jp
LINEスタンプ
エンジニア専用のメイドちゃんスタンプが完成しました!
「あの場面」で思わず使いたくなるようなスタンプから、日常で役立つスタンプを合計40個用意しました。
エンジニアの皆さん、エンジニアでない方もぜひスタンプを確認してみてください。
store.line.me