◆ PNG はほぼ oxipng による最適化で oxipng 単体でも発生した
◆ oxipng 6.0.0 で --libdeflater を設定すると起きるみたい
◆ oxipng 8.0.0 では --libdeflater がデフォルトになったけど 修正があったのか発生していない
◆ 8.0.0 に更新するコミットがすでに行われているので次のバージョンで解決しそう

1 年ちょっと前に Parcel 2 に移行してからハッシュ値が安定しない問題に悩まされてました
同じソースファイルなのにビルドのたびに生成されるファイルのファイル名のハッシュ値が変わります

詳細は別記事で
Parcel2 まだ問題多めな気がする
Parcel のハッシュ値が不安定な問題は環境に依存してそう

2 つめの記事はつい先日のもので 環境依存っぽいし ほぼ諦めていて Vite に移行したいなぁという感じでした
ただまだ少し気になる部分があったのと Parcel の最新の更新でも気になるところがあったのでもう少し調べてみました

発生条件

そもそもこの現象 必ず起きるものでもなく 再現させるのが難しかったりします
実際のファイルが多めのプロジェクトファイルだと 2, 3 回に 1 回くらいの頻度で起きるのに 再現させる最小の構成にすると再現はするものの稀にしか起きません
起きるファイルは 毎回同じというわけではないものの偏りがあって 全く起きないファイルは全く起きないようです

画像ファイルということもあって 画像ファイルそのものによる違いも大きそうということで最も頻繁に発生するファイルを使い回すことにしました
Parcel のビルドは速いと言っても 最小構成でも一回のビルドに 2, 3 秒ほどはかかっています
原因が画像の最適化処理とまではわかっているので Parcel のビルド機能は使わずに 画像の最適化処理だけを呼び出すことにしました

optimizer-image

Parcel の画像の最適化のパッケージはこれです
https://github.com/parcel-bundler/parcel/tree/v2.9.3/packages/optimizers/image

パッケージ名は「@parcel/optimizer-image」です
JavaScript 部分はほぼなくメインは Rust 製のネイティブアドオンです
https://github.com/parcel-bundler/parcel/blob/v2.9.3/packages/optimizers/image/src/lib.rs

中を見ると PNG ファイルはほぼ oxipng に丸投げです
とりあえずこのパッケージを使ってみます

const fs = require("fs")
const optimizer = require("@parcel/optimizer-image/native")

const buf = fs.readFileSync("image.png")
const set = new Set()
let prev = ""

for (let i = 0; i < 1_000_000; i++) {
const result = optimizer.optimize("png", buf)
const b64 = result.toString("base64")
if (b64 !== prev) {
if (!set.has(b64)) {
set.add(b64)
console.log("NEW")
fs.writeFileSync("image-out" + i + ".png", result)
}
console.log(i, b64)
}
prev = b64
if (i % 1000 === 0) console.log("--", i)
}

ループして image.png の最適化を行います
前回と違う結果になったらコンソールに出力します
これで発生するかを確認します

環境依存じゃなさそう

簡単に試せるものが準備できたので 発生頻度が高かった画像を使って 複数の環境で試してみます

すると 先日試したときでは発生しなかったので環境依存そうと判断していた PC でも発生していました
ただ発生頻度は環境次第のようで 10 回のうち複数回発生するものもあれば 2000 回近く実行して 1 回みたいなものもありました
また発生するときは連続や数回置きくらいで発生することがあり 発生しないときは数百回しても発生しないみたいな偏っている傾向がありました

Windows と Linux(WSL) ではどっちでも発生しています
Windows の方が発生しやすいと思っていましたが 今回の試した限りでは Linux の方が発生回数が多かったです

oxipng

上にも書いたように PNG ファイルの最適化はほぼ oxipng 任せです
今度は Parcel 抜きで直接使ってみます

Rust の crate を作ろうかとも思ったのですが oxipng はコマンドラインツールも用意されていて インストールすればコマンドラインから扱えました

cargo install oxipng@6.0.0
oxipng --strip safe --libdeflater --out output.png input.png

現在リリースされている最新の Parcel では oxipng 6.0.0 を使っているのでそれに合わせて少し前のバージョンにしています
--strip safe と --libdeflater は Parcel 側で設定しているオプションで 動作を合わせるためにつけています

何度か実行して出力されたファイルに違いがあったかを調べました
ハッシュ値を比べてみると Parcel 経由で作成したファイルと一致しています
そして 連続して出力するとときどきハッシュ値が異なっているという点も一致していました

ハッシュ値が安定しない問題は oxipng 側の問題だったみたいですね

最新の oxipng では解決してそう

最初 oxipng で作ると Parcel での出力とハッシュ値が一致しませんでした
--libdeflater が漏れていたせいだったのですが これを外すとハッシュ値が異なる問題も起きなくなっていました
--libdeflater 外すと解決できるので外したいのですが Parcel が固定でつけている以上どうしようもできません

だったのですが Parcel の最新の変更点では oxipng を 6.0.0 から 8.0.0 に更新して --libdeflater のオプションが削除されています
現在の最新版 2.9.3 ではまだこの変更はリリースされていませんが 2.9.4 では解決しそうです

https://github.com/parcel-bundler/parcel/commit/2d2400ded4615375ee6bd53ef77b4857ad1591dd
変更理由はこの問題の解決のためではなく aarch64-unknown-linux-musl でビルドに問題があってその解決のためにバージョンを上げたというものみたいです

試しに oxipng 8.0.0 を直接使ってみようと思います
そう思ってインストールしてヘルプを見てみると --libdeflater オプションがそもそもなくなっています
Changelog を見ると --libdeflater がデフォルトになったようです
別のモードになったわけでなく 結局 --libdeflater になっているのだと解決されてなさそうです
しかし 8.0.0 にバージョンが上がっているわけですし --libdeflater の場合でも修正された可能性があります

試してみました
結果 10000 回以上実行してもハッシュ値に変化は見られなかったので 修正されてそうです
また デフォルトで --libdeflater が使われてるはずなのに生成されたファイルのハッシュ値が 6.0.0 の頃と異なっていました

ということなので次のリリースの oxipng が 8.0.0 になってからはハッシュ値が変わってる問題から解放されそうです
もしかするとアルゴリズムの変更で 6.0.0 で発生しやすかったファイルでは発生しなくなっただけで別ファイルだと発生するという可能性もありえますけど ないことを祈ってます