技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

Markdownでスライドを作るツールのFusumaのv2をリリースをしました

リポジトリ: github.com

今回の変更では、面倒事を減らしスライド作成に集中できるようにすることを目標にしていました。

Fusumaとは?

  • #0CJS
  • MDX(Markdown + JSX(optional)) を使いスライドを作れる
  • プレゼンターモードの提供(なぜかTwitter流せたりや落書きもできる)
  • 音声録音とスライドの操作記録を保存できる
  • github pagesへのデプロイがメインで、pdfでも出力可能

大きな変更

og:imageを自動生成/設定するようにしました

ogp用に画像を設定するのがめんどくさいという意見があり、自動的に生成するように追加しました。現在はスライドの1ページ目を画像化しています。

f:id:about_hiroppy:20210203130004p:plain

ユーザー側へCSS Variablesを提供しました

v1では、publicで外に出さずに自由にcssを書いてもらうようにしていましたが、0から書くのはめんどくさい部分もあるので、ある程度の設定は変数に書かれた値を変更すればいいようにしました。それでも、変更が必要な場合はcssで直参照することが可能です。

現在は、以下の値を変更すればよく、多くの場合、cssを0から書く必要はないと思っています。

:root {
  --base-font-family: 'Roboto', 'San Francisco', helvetica, arial, sans-serif;
  --base-font-size: 2.4rem;
  --base-font-weight: 300;
  --base-align: center;
  --base-max-width: 1280px;
  --base-outer-margin: 24px;
  --base-image-height: auto;
  --base-image-width: 100%;
  --base-image-border: none;
  --base-image-border-radius: 0;

  --color-title: #464646;
  --color-base: #545454;
  --color-background: #f5f5f5;
  --color-link: #3498db;

  --h1-font-size: 5.6rem;
  --h1-font-weight: 300;
  --h2-font-size: 4rem;
  --h2-font-weight: 300;
  --h3-font-size: 3.6rem;
  --h3-font-weight: 300;
  --h4-font-size: 3rem;
  --h4-font-weight: 300;
  --h5-font-size: 2.4rem;
  --h5-font-weight: 600;
  --h6-font-size: 2rem;
  --h6-font-weight: 600;

  --code-font-size: 1.8rem;

  --qr-code-image-size: 320px;
}

Webslidesというスライドライブラリをやめました

Webslidesは現在メンテナンスされていないため、Swiperに移行し、必要なcssはfusuma本体で管理するようにしました。これにより無駄なcssが減り全体的な最適化が行いやすくなりました。

swiperjs.com

テーマの追加しました

プリセットのテーマを複数用意しました。(といっても、大半は自分が所属するOSSの色を参考にしました。)
これは、上記のcss variables経由でカスタマイズ可能です。

f:id:about_hiroppy:20210203125420p:plain

こちらのスライドを参考にしてください。

hiroppy.github.io

レンダリング方式を変更しました

og:imageを作成するにあたりbrowserが必要となったため、renderToStringを使うのをやめbrowser側でHTMLを生成するようになりました。これによりHTMLの出力値が少し変わりましたが大きな問題はありません。

a11y(Accessibility)の検証を自動で行うようになりました

browserを動かすことになったのでこのようなことが簡単に行えるようになり導入しました。問題はMarkdownとHTMLの間のsourcemapのようなものがないとhtmlの結果をまんまエラーとして出すことになり、あまり良くないなと思っているのでとりあえず実験的フェーズですが導入されました。

スライドコメントを増やしました

fusumaでは、htmlのコメントシンタックス(<!-- -->)を使い、スライドのカスタマイズを支援します。
v2では、以下のプロパティが追加されました。

  • background
    • v1で消したが戻ってきました
  • block-start, block-end
    • divタグをその区間で生成します
  • executable-code
    • スライド上でjsコードを実行できるようになりました

こちらのスライドを参考にしてください。

hiroppy.github.io

開発環境のときにリファレンスを出すようになりました

自分ですらシンタックスとかクラス名を忘れるので、すぐに見つけれるように。。

f:id:about_hiroppy:20210204001006p:plain


色々と変わって手間を書けずにスライドを作るようになったはずなので、ぜひ試してみてください :)

リリース詳細: github.com

v1のリリース記事

blog.hiroppy.me

GitHub SponsorsによってOSSへの変化はあるのかどうか

数日前にGitHub Sponsorsの機能で企業が支援できるようになることが発表されました。

これにより、今後どのようにOSSに変化があるのかないのかを注目します。

特別視するのが良いアイディアか否か

そこで以下のような面白い議論が発生しました。

これはこの機能が入ったときに、もし支援している企業やユーザーだけがissueを作れて、他の人はコメントだけできるようにする機能が入ったらどうなるかという議論です。

なぜこのようなツイートをこの人がしたかというと以下の理由です。

The goal is to reduce the maintainer's burden. Today the options are: turn off issues completely or make the repo private and give access to sponsors. It would be nice to have a less extreme option.

主な目的はやはりメンテナの負荷を減らすことで、そのためにどう差別化を行うか否かです。

差別化の難しさ

上記の例はかなり極端だと思っています。GitHub側が共通で上記のような設定を加えるとは思っていませんが、各リポジトリで何かしらの差別化を行うかもしれないとは思っています。ただ、OSSでそこまで多くの選択肢はなく、そうなるとコンサルとかそういう部分かなと思います。また、issueの制限を行う等は悪手かなと思います。

他だとぱっと思いつくのは、やはり次のバージョンに向けて機能追加を投票制にするとかでしょうか。

例えば、自分の所属する組織では、機能の追加の優先度を投票制にしています。スポンサーをする場合、一般ユーザーと異なる投票するためのポイントを配分します。もちろん支援してないユーザーでもGitHubを使っている長さに応じ投票するポイントを持っています。

webpack.js.org

他の例だとromeはお金を支払うことにより、会社名(?)をツイート、readmeへの掲載のほか、xx時間の間はマイグレーションのサポートをメンテナが行う等があります。

https://rome.tools/funding/rome.tools

あまり多くの選択肢が無い中、考えつくのはこれぐらいでしょうか。

我々だと一つの企業だけで最大年間$70,000USD(7210000円)支援してもらっていますが、これをどう捉えるかはその組織によるでしょう。自分は多くの会社は見返りを求めて寄付していると思っていませんが、何かしらは提示する必要がある以上、コミットする以外でどうインセンティブを出すべきなのかという答えを見つけていません。

現状

さて、今の世界観は以下の様になっていると自分は思っています。(図が雑。。)

どこのポジションももちろん大切です。developerが物を作らないと、ユーザーというのは獲得出来ませんし、逆にユーザーが使ってくれないとメンテナのモチベーションなどに直結する可能性がありますし、お互い依存されています。また、エバンジェリストはそのOSSを記事を書いたり、登壇したりしてユーザーにわかりやすく伝えます。つまりブリッジのような役割です。

しかしメンテナはどこの組織も常に少ないです。会社のOSSでない場合、どのように新規のメンテナを獲得するかは一つの大切なポイントとなります。どう他の人を巻き込み長期的にメンテナンスできるかを考えなければなりません。というのも当たり前ですが、メンテナは燃え尽きることはもちろんありますし、プライベートの変化等でいなくなる可能性が高いからです。また、仕事でやってない以上それを決めるのは自由だからです。 また義務だと思ってしまうところまで来てしまうと、OSSの楽しさでやっていたはずがそれを失ってしまう可能性がありそれは避けるべきだと思います。

この問題を解決するために、自分が知っているだけでも以下のような仕組みを作っているチームもあります。

blog.hiroppy.me

GitHub Sponsorsにより現状を変えることができるか?

お金が出ることによりメンテナが増えるのでは?というアイディアはあると思います。ただこれにも難しいポイントがあります。

ある有名プロダクトでは過去にコミットした人に対する扱いに対して答えがまだ出てないという話があります。今の人だけに対してお金を配分した場合、過去の人に足して払わないのは問題ではないか?という点です。

また、自分は2019年に同じ組織に復帰して以来、お金をもらってOSSをしてきました。 そこで以下の2点に気付いていました。

お金の計算式が複雑で完璧にはできない

それは、当たり前でコードに対して正当に評価できないからです。いろいろな指標があると思います。コード量、コミット数、PRの数、issueでの対応、など。しかし一貫して言えるのは正確な価値計算ができない点です。

お金が出ても人は辞める

お金はあくまでも一つのインセンティブであり、それをモチベーションにしてOSSをやっている人は多くないというのが肌感です。興味のほうが強いのです。また、やはり仕事やプライベートで時間が取れなくなる例も多く見てきました。あくまでも小さなモチベーションと考え多くを期待することではないと経験上思っています。

しかし、このOSSによって得たお金で大学の学費に当てることができてる方もいます。

なので、これに関しては一概には言えないとも思います。


ただ、逆に組織はお金を得ることによりフルタイムコミッターを作るという方法は大いにあります。自分はGitHub Sponsorsによりこれが全員のメンテナに行える世界が来るのを楽しみにしています。 多くの会社が支援してくれることによりこれは実現可能であり、不可能ではないはずです。

eslintでは、過去に専属メンテナを採用し失敗しました。しかしながら、これはOSS界隈にとっていい経験であり今後につながることでしょう。

eslint.org

We didn't have the budget to pay him a proper full-time salary for his work, so we agreed on 20 hours per week for $5,000/month, with Kai spending the other 20 hours of his week working on other projects or freelancing.

We tried this for a few months before it became apparent that this arrangement wasn't sustainable for Kai. While the payments from ESLint were steady, he had to spend a lot of his other 20 hours each week finding ways to make enough money. So, we updated the arrangement to be for 10 hours each week at $2,500, to give Kai more time to find other work while still providing a baseline amount of dedicated ESLint time.

Unfortunately, that arrangement also did not work.

お金をもらったらそれはOSSなのか?

OSSの定義の話はここではしません。 ところで、Vueの作者のEvanはこのように述べています。

多くの人が最初のツイートを見て、「それはもうOSSではない」というのも目にしますが、それに対する彼の反応は「だからなに?」です。ユーザーはOSSは無料で無制限に使える権利だと思っているが、これらのソフトウェアに割かれる時間やエネルギーは生計を立てる必要がある人から来ている。また、OSSは無償事業だと認識されるが、大勢に使われているソフトウェアは様々な要求や無料の技術サポートの要求に対処しなければならないことがよくある。これが燃え尽き症候群や放棄されたプロジェクトとなる。これが「本物のOSS」かどうかは気にしなく、中間モデルがすべての開発者にプラスの利益をもたらせれる可能性があるかどうかです。これは機能するかどうかわからないが、検討する価値はあります。

自分も開発者からすると、あまりOSSかどうかという点はどっちでもよく、開発しててもプラスになる世界が望む世界かなと思います。eslintの件を見てても、生活資金がOSSだけでは足りなくて打ち切りというのは悲しいです。

しかし企業が参加できるようになったGitHub Sponsorsは、敷居も下がり今後の可能性だと感じています。

さいごに

いずれにせよ、今後企業の参戦によるOSSへの変化を楽しみに見つつ、それに適応できるように最適な解を見つけていくのだろうと思います。これにより、多くのメンテナが少しでも良くなるような世界になれば良いと思っています。

webpack@5の主な変更点まとめ

予定では、明日の10日にwebpackのメジャーバージョンであるv5がリリースされますが、まだエコシステムが安定していない可能性があるため、注意してアップグレードを行ってください。

github.com

change log:

github.com

移行ガイド:

webpack.js.org

追加機能

Persistent Caching

このバージョンからは今までメモリ上でしか行ってなかったファイルシステムによるキャッシュが導入されます。以下のように設定することにより、大幅な速度改善が見込めます。

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
};

なし ↓

asset main.js 36.3 KiB [emitted] [minimized] (name: main)
orphan modules 584 KiB [orphan] 554 modules
cacheable modules 117 KiB
  ./src/index.js + 103 modules 117 KiB [built] [code generated]
  ./src/foo.js 21 bytes [built] [code generated]
webpack 5.0.0-rc.2 compiled successfully in 1836 ms

あり↓

asset main.js 36.3 KiB [compared for emit] [minimized] (name: main)
cached modules 700 KiB [cached] 556 modules
webpack 5.0.0-rc.2 compiled successfully in 429 ms

詳しくは以下の記事を参照にしてください。

blog.hiroppy.me

Module Federation

リポジトリ間(バンドル間)を跨ぐときにライブラリなどの重複しているコードを以下のように効率よく扱いバンドルサイズを下げる仕組みです。この機能はお互いのwebpackと連携を取り合う必要があるため互いにwebpack@5である必要があります。

https://cdn-ak.f.st-hatena.com/images/fotolife/a/about_hiroppy/20200507/20200507073925.png

詳しくは以下の記事を参考にしてください。

blog.hiroppy.me

assetModules typeの追加

今まで画像などを読み込むときに、file-loaderやurl-loader, raw-loaderなどを使っていましたがそれがネイティブサポートされました。

module.exports = {
  output: {
    assetModuleFilename: 'images/[hash][ext]',
  },
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset/resource'
      }
    ]
  }
};

詳しくは以下の記事を参考にしてください。

blog.hiroppy.me

チャンク名がIDへ変更

今まで以下のようにwebpackChunkNameと書かなければ読めないファイル名となっていましたが、人が読める形となります。それに伴い、開発中でのwebpackChunkNameの指定をする必要がなくなることが期待されます。

(async () => {
  await import(/* webpackChunkName: "foo" */ './foo');
})();

名前をつけたときの出力

asset main.js 2.79 KiB [emitted] [minimized] (name: main)
asset foo.js 114 bytes [emitted] [minimized] (name: foo)
runtime modules 7.23 KiB 10 modules
cacheable modules 217 bytes
  ./src/index.js 190 bytes [built] [code generated]
  ./src/foo.js 27 bytes [built] [code generated]
webpack 5.0.0-rc.4 compiled successfully in 274 ms

v5のデフォルトでは以下のようにdeterministicという設定の新しいアルゴリズムが追加され、モジュール/チャンクの名前に3~4桁の数値IDが付与されるようになります。これにより、ハッシュ化されたモジュールIDによるgzipでのパフォーマンス低下は修正されました。

asset main.js 2.79 KiB [emitted] [minimized] (name: main)
asset 717.js 114 bytes [emitted] [minimized]
runtime modules 7.23 KiB 10 modules
cacheable modules 186 bytes
  ./src/index.js 159 bytes [built] [code generated]
  ./src/foo.js 27 bytes [built] [code generated]
webpack 5.0.0-rc.4 compiled successfully in 283 ms

ファイル名を自動的に付与したい場合

module.exports = {
  optimization: {
    chunkIds: 'named'
  }
};
asset main.js 2.8 KiB [emitted] [minimized] (name: main)
asset src_foo_js.js 123 bytes [emitted] [minimized]
runtime modules 7.23 KiB 10 modules
cacheable modules 186 bytes
  ./src/index.js 159 bytes [built] [code generated]
  ./src/foo.js 27 bytes [built] [code generated]
webpack 5.0.0-rc.4 compiled successfully in 256 ms

optimization.chunkIdsnamed を追加すればファイル名が確定しますが本番環境では表示されていいものなのかを検討してください。また、optimization.splitChunks.nameはなくなったのでこちらに移行してください。

import.metaのサポート

// ./src/index.js
console.log(import.meta.url);
console.log(import.meta.webpack);
// ./dist/main.js 
// 生成されたファイルは固定値として入り、import.meta.url, webpackは存在しなくなる
console.log("file:///Users/hiroppy/webpack/src/index.js");
console.log(5); 

また、HMR時に今までは以下のように書いていましたが、これからはimport.meta.webpackHotを使うことが可能です。これを使うことにより、Node.jsのmoduleへの依存を減らし、ESMに沿うような書き方に変わります。

// <= 4
if (module.hot) {
  module.hot.accept();
}

// >= 5
if(import.meta.webpackHot) {
  import.meta.webpackHot.accept();
}

// or
import.meta.webpackHot?.accept(); 

data, file, http(s)プロトコルのサポート

import x from 'data:text/javascript,export default 42';
console.log(x); // 42

import y from 'file:///Users/hiroppy/webpack/src/index.js';

また、フラグメント(#)もサポートされました。

const eIndexOf = require('es5-ext/array/\0#/e-index-of#fragment');

http(s)プロトコルは、まだ完全にサポートされていないため以下の設定が必要です。

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.experiments.schemes.HttpUriPlugin(),
    new webpack.experiments.schemes.HttpsUriPlugin()
  ]
};

// index.js
import codeOfConduct from 'https://raw.githubusercontent.com/webpack/webpack/master/CODE_OF_CONDUCT.md';
console.log(codeOfConduct);

Native Workerのサポート

new Worker(new URL('...', import.meta.url))がWebWorkerを作るようにサポートされました。これはSharedWorkerも同様です。

const fooWorker = new SharedWorker(new URL("./foo-worker.js", import.meta.url), {
  name: 'foo'
});

publicPathの自動化

新しくデフォルト値としてautoが追加され、document.currentScript, document.getElementsByTagName('script'), self.location の中から自動的に決定されます。注意点として、IEではdocument.currentScriptがサポートされていないため、deferred か async のスクリプトには使用することができません。

module.exports = {
  output: {
    publicPath: 'auto'
  }
};

Tree Shakingの最適化

ネストされたモジュールの場合、今までは使われていないbは削除できませんでしたがv5からは追跡可能となりできるようになりました。

// inner.js
export const a = 1;
export const b = 2;

// module.js
import * as inner from "./inner";
export { inner }

// user.js
import * as module from "./module";
console.log(module.inner.a);

v4では、モジュールの関係性しか見ていませんでしたが、v5から入ったoptimization.innerGraphにより、内部モジュールへの最適化も行えるようになりました。

import { something } from "./something";

function usingSomething() {
  return something;
}

export function test() {
  return usingSomething();
}

以下のケースが対象です。

  • 関数宣言
  • クラス宣言
  • 変数宣言 及び export default

Optimization.sideEffectsでは、ソースコードから副作用のないモジュールの単純なケースを検出できるようになりました。クラスおよび関数宣言、簡単なinit式を使用した変数宣言、ifwhileforswitchexportimport、簡単なフラグを使用した関数呼び出し 等です。

また、CJSもサポートされました。

  • module.exports = require('...')
  • module.exports.a.b.c = require('...').a.b.c
  • Object.defineProperty(module.exports, 'xxx', ...)
  • require('abc').xxx

このサポートは、ESM、CJS間でも動くので、今後どちらのモジュールシステムを使っているかを気にせずに最適化行えるようになります。

これは別の記事で詳細に説明するので予定です。

output.filename, output. chunkFilenameの関数化

output.filenameは今まで文字列しか受け取りませんでしたが、関数にすることが可能となったため更に柔軟な設定を表現することが可能となります。

module.exports = {
  output: {
    filename: ({ chunk }) => {
      if (chunk.name === 'main') return 'main.bundle.[contenthash].js';
      return 'foo.bundle.[contenthash].js'
    }
  }
};

externalsTypeの追加

externalsTypepromise, import, scriptが追加され、より柔軟に対応できるようになりました。

  • promise: varと同様だが、非同期モジュールとなる
  • import: import()を使い、非同期のネイティブESMモジュールを読み込む
  • script: <script>を使い、事前に定義されたグローバル変数を公開するスクリプトを読み込む
module.exports = {
  externalsType: 'promise'
};

targetの詳細化とbrowserslistのサポート

targetに対して、詳細な設定ができるようになりました。 配列を受け取るようになり、target: ['web', 'es2015'] 等の書き方が行えるようになりました。 また、browserslistがされたため、webの場合はtargetの設定は不要となります。

github.com

デフォルト値はtarget: 'browserslist'となり、フォールバック先は変わらずにwebとなります。

TypeScript型定義ファイルの提供

@types/webpackは不要になりました。

import { WebpackOptionsNormalized } from 'webpack';

const config: WebpackOptionsNormalized = {
  entry: 'index.js',
  output: {
    filename: 'bundle.js'
  }
};

splitChunksでのサイズ設定値の変更

今までは、JSのみのチャンクサイズでしたが、さらに詳細に指定できるようになりました。

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        test: {
           name: 'test',
           minSize: {
             javascript: 100,
             webassembly: 100,
             style: 100,
          }
        }
      }
    }
  }
};

また、本番環境でのminSizeのデフォルト値は20kとなりました。

実験的段階

top-level-awaitのサポート

シンタックスはESMの仕様に沿いますが、まだstage-3なので実験的フェーズです。

github.com

// webpack.config.js
module.exports = {
  experiments: {
    topLevelAwait: true,
  }
};
const x = await import('file:///Users/hiroppy/Desktop/webpack-5/src/foo.js');

console.log(x);

scriptタグでのモジュールサポート

バンドル時に使われるIIFEが取り除かれ、<script type="module">経由で呼び出される形に出力されます。この場合、仕様に沿い厳格モードと遅延ロードが有効化されます。

module.exports = {
  experiments: {
    outputModule: true
  }
};

破壊的変更

最低要求バージョンがNode.js@10へ

webpack及びwebpackのコアにおけるエコシステムが要求するNode.jsのバージョンの最低値は10となります。

Node.jsのpolyfillの自動挿入が廃止

メンバー間でも賛否両論がありましたが、理由としては以下のような目的があります。

  • webpackはwebへ向かっている
  • polyfill自体が完全互換なものではない
  • メンテナンスコストの高さ

自分が経験した例としては、processutilに依存しているNode.jsのコードをクライアントサイドで使う場合があり、v5に上げたら動かなくなる場合があります。

実際にwebpack4まで使っていたpolyfillは以下のリポジトリで管理されているので、これを参考にして各自で追加する必要があります。

github.com

これに伴い、node.*の中のネイティブモジュールがすべて廃止となります。 また、global, __filename, __dirnameはデフォルトでfalseの値となります。

module.exports = {
  node: {
    // Buffer: false, これは廃止
    global: false,
    __filename: false,
    __dirname: false,
  }
};

JSONでのnamed exportの禁止

ESMの仕様上、これは許可されていないためこれが行われているコードの場合警告が出るようになるため、以下のように変更する必要があります。

// 😵
import { version } from './package.json';

// 🙂
import package from './package.json';
const { version } = package;

loaderとuseの違いを厳格化

rules.loaderrules.useで目的に合ってない使い方の設定の場合、エラーを吐くようになりました。 useoptionsがない場合のみ使用可能(引数は受け入れ可)となり、optionsがある場合はloaderを使わなければなりません。

デフォルトランタイムが一部ES2015へ変更

webpackの生成するコードのデフォルトが一部es5からes2015となります。
これはあくまでもバンドルサイズを減らすことが目的なため、varからconstにはなったりせず、function() => {} となります。
もしIEをサポートしている場合は以下を追加する必要があります。

module.exports = {
  target: ['web', 'es5']
};

また、これは追加機能として用意されたbrowserslistを用いて回避することも可能です。

# browserslist
last 1 version

webpack@5で入るPersistent Cachingについて

webpackはin-memoryのみで今まで永続的なキャッシュを実装していませんでした。理由としては、パフォーマンスよりも安全性を優先していたためです。
cache-loaderを使ったことがある人はわかるかもしれませんが、確かに速くなる一方、安全性が損なわれているのは事実です。

この機能は、webpackはデフォルトでファイルキャッシュをオンにはしませんがそれでもビルドの速度を上げたい場合に使う機能です。

以下がデフォルトの挙動となります。

mode cache
development memory
production false

https://github.com/webpack/webpack/blob/60f7ce301df553d8ab0276ba2838ddb60ead1c94/lib/config/defaults.js#L158-L160

実際に使うときの設定

結論ですが、webpack.config.jsへ以下のように書くことが推奨されます。

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
};

あとは、各コードの設定に依存するためversion等の追加が必要になる可能性があります。

ドキュメント

webpack.js.org

仕組み

ファイルキャッシュでは以下のようにデフォルトではnode_modules/.cache/webpackというディレクトリにスナップショットを生成します。

(๑˃̵ᴗ˂̵)و ~/D/w/node_modules  ᐅ tree .cache
.cache
└── webpack
    └── default-production
        ├── 0.pack <-- 生成済みコードの記録
        └── index.pack <-- 依存ファイル等の記録

2 directories, 2 files

このようにシリアル化されたデータを保存します。
MD4ハッシュアルゴリズムを用いたetagを識別子とし、それと一致したものをwebpackは使用します。(実装が知りたい人はcreateHash.jsPackFileCacheStrategy.jsを読んでください)
本番環境とbuildDependenciesには、 timestamp + hash モードがデフォルトで適応されます。
使うユーザーは snapshot のオプションを設定することはないと思うので、この記事では割愛します。 https://webpack.js.org/configuration/other-options/#snapshot

webpackは、すべてのモジュールそれぞれに対し、compilation.fileDependencies, compilation.contextDependencies, compilation.missingDependenciesをトラッキングし、スナップショットを生成していきます。 余談ですが、この三点は、webpack@5からSortable SetからSetとなり、並び替えが不可能となりましたのでプラグイン作者は気をつけてください。

つまり、特定ファイルを変更するとwebpackはそのファイルのキャッシュ(webpack内ではキャッシュエントリと呼ばれる)を無効化し、各種loaderを実行後にファイル解析を行い再生成を行います。また、bundle.jsのキャッシュエントリを無効化し、このファイルを再生成する可能性があります。

例えば、依存ファイルを変更すると以下のように変更されます。

(๑˃̵ᴗ˂̵)و ~/D/w/node_modules  ᐅ tree .cache
.cache
└── webpack
    └── default-production
        ├── 0.pack
        ├── 1.pack
        ├── index.pack
        └── index.pack.old

2 directories, 4 files

1の方が新しくなり、スナップショットが追加されました。

キャッシュエントリが無効化されるケース

以下の場合にキャッシュエントリが無効化されます。

  • 監視下のファイルが変更されたとき
  • 設定を変更したとき
    • webpack.config.jsのcache等の設定変更
  • loaderかpluginがパッケージアップデートされたとき
  • 依存関係(node_modules)がパッケージアップデートされたとき
  • cliからビルドに影響のある値を送ったとき
    • --optimization-minimize, コードで判断できないもの
  • カスタムなビルドスクリプトが変更されたとき
    • cache.version, cache.name, cache.buildDependencies

ビルド結果を変更する可能性のあるものはキャッシュエントリが無効化されます。
例えば、--optimization-minimizeを渡せばビルド結果には影響されます。しかし入力されたソースコードの変更だけではこれを検知できませんが、キャッシュはこれを考慮する必要があります。 webpackではそれに対して、cache.version, cache.name, cache.buildDependenciesを使い処理をしますが、これを自動的に認識するのは難しいため影響が生じたときに再構築する必要が出てきます。(かなり安全性を重視しています)

オプション

最低限のものだけ説明します。

type

memoryfilesystemが存在し、どちらを選択することができます。

buildDependencies

cache.buildDependenciesには、ビルドにおけるコード依存関係を追加します。

defaultWebpack

webpackのすべての依存関係を取得するために、デフォルトでwebpack/libとなります。
この設定は基本的に設定する必要はないです。

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      defaultWebpack: ['webpack/lib'],
    }
  }
};

config

公式では、最新の設定(webpack.config.js, etc)とすべての依存を取得するために__ filenameを設定することが推奨されます。
このように書くことにより、設定とすべての依存関係を取得するようになります。

Other Options | webpack

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
};

ディレクトリの場合、最も近いpackage.jsonの依存関係が分析され、ファイルの場合は、Node.jsのモジュールのキャッシュを見て、依存関係をwebpackは把握します。

注意点として、ディレクトリの場合は、必ずスラッシュで終わる必要があります(そうしないとファイルと識別されてしまう)

version

たとえ同じ内容でも、この値を変更することにより永続的キャッシュを無効化することができます。
ビルドの一部の依存において、表現できない場合が存在します。(e.g. DBから読み込まれた値、環境変数コマンドラインで渡される値)
もしキャッシュがおかしい場合このオプションを確認・検討してください。

もしコードがdefinePlugin経由で環境変数を入れていてそれをバンドルに埋め込む場合、これはこの環境変数(e.g. gitのrevision)への依存があるので、バージョン名をこの値にし、キャッシュを無効化する必要があります。

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.foo': JSON.stringify('foo')
    })
  ]
};

上記の場合はconfigでこのwebpack.config.jsを監視下に置いており、そこでfooを定義しているため、この文字列を変更したらwebpackは検知できるため問題ないです。問題は、監視下でwebpackが変更されたか認知できない(ずっと末端まで変数等)です。

しかし、.envを利用する場合は以下のようにversionを指定しないとキャッシュは更新されません。

# .env
VERSION=1.0
const webpack = require('webpack');
const { config } = require('dotenv');

config();

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    },
    version: process.env.VERSION
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.version': process.env.VERSION
    })
  ]
};

さてこれで問題になるのが、もしこれをしっかり設定しないと過去のスナップショットを参照するため生成結果も過去の状態になることです。これがwebpackが恐れていた問題です。この場合だと、versionを指定しない場合、出力は常に最初のビルド時のものとなり.envで書き換えしても反映されません。

つまり、cache.nameでも、キャッシュを無効化できるためコードに依存しますが導入の検討する可能性があります。

パフォーマンスの最適化

node_modulesのコードに対して、timestamp + hashで管理するとコストがかかりビルド速度が低下するためwebpackでは、package.json内のバージョンと名前を利用し評価しています。 なので、絶対にnode_modules内のコードを編集することは避けてください。

この最適化は、snapshot.managedPathsのパスに適応され、デフォルトではwebpackがインストールされているnode_modulesとなります。yarn.pnpの場合、ファイルパスでハッシュを利用するため上記の最適化はyarnがカバーするため行われません。

d3を使った場合のパフォーマンス測定

// index.js
import  * as d3 from 'd3';
import 'foo.js';
// foo.js
console.log('foo');
// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
};

最初はキャッシュがないため、webpack@4同様に以下の速度ぐらいとなります。

asset main.js 36.3 KiB [emitted] [minimized] (name: main)
orphan modules 584 KiB [orphan] 554 modules
cacheable modules 117 KiB
  ./src/index.js + 103 modules 117 KiB [built] [code generated]
  ./src/foo.js 21 bytes [built] [code generated]
webpack 5.0.0-rc.2 compiled successfully in 1836 ms

もう一度、何も変更せずにビルドを行ってみます。

asset main.js 36.3 KiB [compared for emit] [minimized] (name: main)
cached modules 700 KiB [cached] 556 modules
webpack 5.0.0-rc.2 compiled successfully in 429 ms

前回と同様なのですべてキャッシュが使われていることがわかり、約4.5倍程度早くなったことが確認できます。

それでは、foo.jsの中身を変更します。

asset main.js 36.3 KiB [emitted] [minimized] (name: main)
cached modules 700 KiB [cached] 555 modules
./src/foo.js 18 bytes [built] [code generated]
webpack 5.0.0-rc.2 compiled successfully in 1228 ms

foo.jsのコードのみが再度生成され、foo.jsのスナップショットが更新されました。index.jsを含め更新されたわけではないため速度はフルビルドの時よりも速くなります。

再度、何も変更せずにビルドを行います。

asset main.js 36.3 KiB [compared for emit] [minimized] (name: main)
cached modules 700 KiB [cached] 556 modules
webpack 5.0.0-rc.2 compiled successfully in 416 ms

すべてのキャッシュが利用され変更がないため、400ms台で落ち着きました。

さいごに

webpack5への機能追加として、一番投票率が多かったのがこの永続的キャッシュという機能でした。

webpack.js.org

この機能は、開発時に大いに役に立つと思います。もし、webpackのビルド時間に不満がある人はこの機能を試してみると良いかなと思います。

rendertronを用いてSSRに対応してないサイトでもSEOやOGP対策を行う

Dynamic Rendering

この手法はDynamic Renderingと呼ばれ、SSRに対応してないサイトに対してのSEO対策として有効です。Dynamic Renderingとは一言でいうと、サーバーでNode単体ではなく、ブラウザを動かすイメージです。 これはSSRみたいなNode.jsのコードを書くことないため、導入コストは低いです。

詳しくは、以下のgoogleの記事を読んでください。

developers.google.com

この記事でも説明されているrendertronを今回は用います。

Rendertron

github.com

puppeteerをラップしたapi serverみたいなもので内部はkoaが使われています。これを起動し、/renderへurlをpathとして挿入するとそのページのhtmlが返されます。 例えば、/render/https://google.comとアクセスすると、google.comのhtmlが返ってきます。 また、スクリーンショットも取れたりします。(/screenshot)

返すhtmlは配信元とは一致はせず最適化されたものが返されます。例えば、console.log('hello')document.write('test')だけ書かれたjsなどは、htmlに挿入された後そのスクリプトタグはhtml内からなくなったり、baseがついたりします。

ちなみにrendertronをGCPで動かすのはもっと簡単だったりします。

インフラ構成

github.com

上記のリポジトリではdocker-composeで簡単な構成を作りました。

  • https://foo.comへアクセスが来たとき、Nginxでbotかどうかを判断する
    • botの場合は、rendertron(internal)のサーバーへ
      • アクセスのurlをrendertronのurlのpathにつける
        • e.g. http://rendertron/render/https://foo.com
      • rendetronがindex.htmlへアクセスし、htmlをレンダリングし返す
    • ユーザーの場合は、index.htmlを取りに行く

前段

以下を参考にしました。

github.com

upstream rendertron {
  server rendertron:3000;
}

map $http_user_agent $is_bot {
    # default 1; # if you want to debug as a bot, you should comment out this
  '~*googlebot' 1;
}

server {
  listen 80;
  server_name localhost;

  if ($is_bot = 1) {
    rewrite ^(.*)$ /rendertron/$1;
  }

  location /rendertron/ {
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_pass http://rendertron/render/$scheme://storage$request_uri;
  }

  location / {
    proxy_pass http://storage/;
  }
}

botの場合、urlに/rendertronを付け、location /rendertron/の分岐へいれます。そして、proxy_pass http://rendertron/render/$scheme://storage$request_uri; のリバースプロキシを設定します。 このように書くことにより、http://localhost:8080http://rendertron/render/http://localhost:8080と飛ばすようにし、htmlを返すようにします。

Rendertron

特に何もしなくていいですが、puppeteerを導入するために自前でDockerfileを書くのは少し大変なので、今回は、こちらのイメージを使いました。

hub.docker.com

SPA

index.htmlを持っているサーバーでtry_filesしてあげることにより、404を回避させます。

# nginx.conf

server {
  listen 80;
  server_name localhost;

  location / {
    root /usr/share/nginx/sample;
    # for spa
    try_files $uri $uri/ /index.html;
  }
}

HTML, JS

ここは例なので何でもよく、各サービスのアプリケーションコードとなります。
今回は、重いアプリケーションを動かしたかったのでここからd3のサンプルを借りました。 これがhtmlへレンダリングされていれば成功となります。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
  </head>
  <body>
    <div id="observablehq-6b3f2a05"></div>
    <script type="module">
      (async () => {
        const id = '#observablehq-6b3f2a05';

        if (document.querySelector(id).children.length === 0) {
          const { Runtime, Inspector } = await import('https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js');
          const { default: define } = await import('https://api.observablehq.com/@d3/hierarchical-edge-bundling.js?v=3');
          const inspect = Inspector.into(id);

          (new Runtime).module(define, name => name === 'chart' ? inspect() : undefined);
        }
      })();
    </script>
    <script src="main.js"></script>
  </body>
</html>

main.jsでは、ogpを設定したいと思います。

// main.js
const a = document.createElement('a');

a.setAttribute('href', 'https://observablehq.com/@d3/hierarchical-edge-bundling');
a.text = 'This is hierarchical-edge-bundling, the code is here.';

document.body.append(a);

// ogp
const props = [
  {
    type: 'og:url',
    content: 'http://localhost:8080'
  },
  {
    type: 'og:type',
    content: 'website'
  },
  ...
];

const fragment = document.createDocumentFragment();

props.forEach(({ type, content }) => {
  const meta = document.createElement('meta');

  meta.setAttribute('property', type);
  meta.setAttribute('content', content);

  fragment.appendChild(meta);
});

document.querySelector('head').appendChild(fragment);

結果

ユーザーがアクセスした場合

まんま上記のhtmlが出力されただけとなり、CSRです。

botがアクセスした場合

metaにogや bodyの中にd3の結果出力コードが出ていてSSRが成功しました。

また、画面でみてもユーザーのアクセスと同様の画面となります。

これでgoogleボットやTwitterなどのogpにも対応することが可能です。

問題点

体感的に、SSRよりは遅く感じます。SSRは最適化しやすいのもあると思いますが。

SSRよりは楽な分、効率が悪いようにみえますが、今後ssr-serverとrendertronで同じ鯖スペックでどれぐらい捌けるのかも含め実験してみたいなーって思ったりします。

いずれにせよこういうのは、大規模サービスで実験しないとわからないことが多いので今後に期待です。

さいごに

昨日の夜、突然やりたくなって記事にしました。

導入コストは低いので、今SPAなサイトだけどSSRしてないからSEOが不安とかogpも有効化したい!って人は検討してみてもいいんじゃないでしょうか。

リポジトリ

github.com

初めて大腸内視鏡検査を受けた

次回受ける時に、忘れないようにするために記事にします。今終わって病院で意識が朦朧としてますがブログを書けるぐらいには意識があります。

そもそもなんで自分から受けたのか?

安い馬肉食べたら次の日トイレに篭りっぱなしの状況でそこから1ヶ月以上、体調が良くならなかったので、せっかくの機会ということで一度受けてみようかと思い受けることにした。

スケジュール

まずは最初に採血とエコーを受ける必要がありました。

  • 08/04: 採血とエコーを受け、エコーの結果を受け取る
  • 08/12: 食事制限が始まり、消化がいいのもを食べる
  • 08/13: 夕飯後に下剤を飲む
    • 朝: 食パン2枚
    • 昼: 食パン3枚
    • 夜: そうめん150g
  • 8/14:
    • 家で下剤1800mlをゆっくり飲む(08:30 ~ 10:30)
    • 下剤のせいでトイレに引きこもって様子を見る
    • 13:00に病院に行き、再チェック -> 自分の場合ダメで追い下剤で目が死んだ
    • 14:10に検査開始
    • 14:35?に検査終了
    • 15:10にある程度意識が復帰して自分で歩けるようになったので支払いして家に帰る

内視鏡検査はしんどいのか?

一番しんどいのは、明らかに下剤を飲んで腸をクリーンアップするステップだと思う。 味はスポーツドリンク味! そして、下剤を飲むとトイレに引きこもることになる。

検査は全く覚えてないし、しんどくない。

検査中

最初に、検査着に着替えるが、下のショートパンツは少し特殊で後ろが開くような作りになっている(それはそう) ただ、糸で結んであり開けるときはその紐を解くので他の人から見られるとかはない。

検査室に入ると、名前と生年月日の確認が行われる。 その後に看護師さんが二人くる。その後にベッドに仰向けになり、右手に鎮痛剤(麻酔みたいなものだがウトウトする感じ)を静脈注射されている間に、緑内障とか血をサラサラする薬を飲んでいるか?とかの質問に答えていく。 ちなみにこの注射は全く痛くない。 そして、右向きに体勢を変えて先生が来るのを待つ。

数分後に先生が来て、鎮痛剤を流しますねって言われる。(これは先生しかできないらしい) 自分でも、1,2秒なにか液体が流れてきたという感覚を覚えているがその後の記憶はなく、起こされたら終わっていた。。

終わった後に、休憩室に案内されたときは自分ひとりでは歩けずに看護師さんに手伝ってもらったがここの記憶もかなり曖昧でなんとなく覚えている感じ。

その後に、意識がまだ若干曖昧な感じではあったが診察室に呼び出されて結果を聞き、また休憩室で休んだ。

正直、全く処置に関して覚えてないので痛いという感情がない。

気をつけたいこと

前日に飲む下剤を早く飲みすぎない

自分は、この薬を13日の19時に飲み、23時に寝たところ深夜2時ぐらいに腸が動きまくり寝れる状態ではなくなった。朝6時にもこれで起きた。
つまり、前日に早く飲みすぎないほうがいいのではと思っている。

硬いトイレットペーパーは使わない

以前、世間的にトイレットペーパーがなくなった騒動があり、そのときに買ったものがまだ余っていたのでトイレに籠っている間使ってみたのだが、肌を痛めるので絶対に使うべきではない。このときのトイレットペーパーの質悪すぎる。。。

さいごに

次、受けたとしても自分の感想をここに残しているので多分普通に受けれると思う。
内視鏡検査ってすごい怖いイメージあったけど、そもそも検査中に意識ないから怖いもクソもなかったことがわかってよかった。
初めて麻酔みたいな意識が飛ぶ体験をしたが、すごいって感想しかない。
三年に一回受けたほうがいいらしいので、30歳になったらまた受けようかなと思う。

p.s. endoscopeと英語で言うらしい、覚えやすい!

webpackの次のバージョンで入るassetModulesの紹介

この機能が導入されることにより、{raw/file/url}-loaderが不要となります。
webpack@4でも使えますが、まだ実験的フェーズです。

Documentation

webpack.js.org

PR

github.com

モジュールタイプと以前との対応表

  • asset/resource -> file-loader
  • asset/inline -> url-loader
  • asset/source -> raw-loader
  • asset -> asset/resourceasset/inlineを自動選択する(閾値: 8kb)

使い方

実験フラグをオンにする。

// webpack.config.js
module.exports = {
  experiments: {
    asset: true
  },
};

アセットを出力する (file-loader)

ディレクトリにファイルを出力しそのファイルパスを返す。
output.assetModuleFilenameに出力ファイル名を指定することができる。

// webpack.config.js
module.exports = {
  output: {
    assetModuleFilename: 'images/[hash][ext]',
  },
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset/resource'
      }
    ]
  },
};
// src/index.js
import foo from './images/foo.png';

console.log(foo);
// /dist/images/151cfcfa1bd74779aadb.png
img.src = foo;

データURIを出力する (url-loader)

Base64アルゴリズムを使い出力される文字列を返す。(カスタマイズ可能)

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset/inline'
      }
    ]
  },
};
// src/index.js
import foo from './images/foo.svg';

console.log(foo);
// data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=

block.style.background = `url(${foo})`;

ソースを出力する (raw-loader)

ソースコードの中身を返す。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.txt/,
        type: 'asset/source',
      }
    ]
  },
};
// src/index.js
import foo from './foo.txt';

console.log(foo);
// hello!
block.textContent = foo;

自動的に選択させる

ファイルの大きさにより、自動的にasset/resourceasset/inlineの実行を決定する。
8kb以下の場合は、inlineとなりそれ以上はresouceとなる。

閾値を変えたい場合は、parser.dataUrlCondition.maxSizeを指定することができる。

module.exports = {
  output: {
    assetModuleFilename: 'images/[hash][ext]',
  },
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024, // 4kb
          },
        },
      },
    ],
  },
};