😱

React/React Native/TypeScriptの圧縮ライブラリ 簡易まとめ

2022/06/27に公開

こんにちは、最近フロントエンドと仲良くなってきたむっそです。

もともとバックエンドばかりやっていたのですが、仕事の都合でフロントエンド(React/React Native/TypeScript)も触ることが多く、今回は 便利そうな画像/ファイル圧縮ライブラリを調べてみました。

はじめに

フロントエンド側で大きめなファイル(画像やPDF/CSVなど)を受け取った時に以下のような流れでシステムを組むことが多いのかなぁと思います

フロントエンド ⇒ バックエンド(圧縮処理) ⇒ インフラ(S3など)

ただ個人的に 「ネットワークがボトルネックになることが多いので、極力ネットワークを経由したくないし、早めに圧縮したいなぁ」 っていうのが正直な気持ちです。
となると、理想的な流れは以下のような感じかと思います。

フロントエンド(圧縮処理) ⇒ インフラ(S3など)

だいぶすっきりしますね。
しかも最近のフロントエンド側の計算機(スマホやPC)も割と高性能だと思いますし、 計算処理が重くなる圧縮処理でも多少耐えられるのでは? というのが私の考えです。(独断と偏見です)

ということでフロントエンド側で圧縮処理できるライブラリをいくつか調べました。

使用環境

ウェブアプリケーションはReact/TypeScript、モバイルアプリケーションはExpo/React Native/TypeScriptを使用しています。

OS: MacOS Big Sur version 11.6
メモリ: 8GB
チップ: Apple M1

Node version:14.18.2
expo version:44.0.1
TypeScript version:4.5.5

結論(早く答えが知りたい方へ)

開発環境やバージョンに深く依存するとは思いますが、私の環境では下記の圧縮ライブラリがちょうど良さそうでした。

画像圧縮ライブラリ

※Expo/ReactNativeという環境で使用していますが、ReactNativeのみの環境であれば他のライブラリのほうが良いかもしれません。

ファイル圧縮ライブラリ(gzip)

  • ウェブアプリケーション:
    fflateが良さそう

  • モバイルアプリケーション:
    モバイルでファイル圧縮しなくても大丈夫だったので今回は調査してません。
    良いライブラリがあれば教えてください...

ということで以下の章では、画像圧縮ライブラリとファイル圧縮ライブラリ、それぞれでいろんなライブラリを調べて選定理由や実装を載せていきます。
技術選定理由がガバガバなものもありますが、優しいコメントお待ちしております。笑

画像圧縮ライブラリ編

どうやって圧縮していくか方針を決める

  • 下記のgoogle PageSpeed Insightsのページがすごく参考になります。
    インターネットの画像トラフィックは全体の96%なんですね...これはちゃんと最適化しないとやばそう。

GIF、PNG、JPEG の各形式は、インターネットの画像トラフィック全体の 96% を占めています。

https://developers.google.com/speed/docs/insights/OptimizeImages

  • 画像はすでに圧縮されているから 画像サイズを落とすの無理では? っと思ったのですが、いくつかやり方はあるようです。たとえばjpegの場合だと、google PageSpeed Insightsのページでこう書かれています。

画質が 85 を超えている場合は、85 に下げます。画質が 85 を超えると画像の容量が急増しますが、視覚的な品質はほとんど向上しません。

画像品質100% という設定にしたいですが、そんなに劣化がないのであれば画像品質85%で良さそうですね。業務に影響を与える可能性も少なからずあるので、品質劣化が許容できるかどうかはぜひ検証してみてください。

https://parashuto.com/rriver/development/optimizing-jpeg-images-with-85-percent-quality

  • Squooshでいろんな画像圧縮を試すことができます。

jpegであればmozJPEG、pngであればOxiPNGに変換することで大幅にサイズを縮小できます。圧縮効率が良いアルゴリズムに変換するという方向性でも良さそうです。

ためしにjpegファイルをmozJPEGに変換すると
1.62MB ⇒ 154KB(圧縮率90%)まで圧縮できます。
えげつねぇぇぇ

ウェブアプリケーション編

ライブラリの種類と選定理由

  • browser-image-compression
    ⇒githubで圧縮を試せるデモページを用意している、typescript、マルチスレッドにも対応。いくつか検証して圧縮率が70-80%だったので採用。

  • JavaScript-Load-Image
    ⇒圧縮というより画像の切り取りなどがメインのため採用断念。

  • compressorjs
    ⇒typescriptに対応してなさそうだったので採用断念。

  • Squoosh
    ⇒Node.js 16.5未満のバージョンでWasmに関する不具合があるらしい。そして開発環境のNodeバージョンが残念ながら16.5未満なので採用断念。
    mozjpegなどの高圧縮アルゴリズムなども用意していて良さそうなのでNodeのバージョンが上がり次第採用しても良さそう

https://ics.media/entry/220204/

※注意:Appleシリコン搭載Mac(M1 Mac)ではNode.js 16.5未満のバージョンでWasmに関する不具合があり、処理できない場合があります。Appleシリコン版MacでlibSquooshを利用する際はNode.js 16.5以上のバージョンをインストールしてください。

実装(util/compress.ts)

browser-image-compressionを使用してutilを実装します。

import imageCompression from "browser-image-compression";

export type compressImageType = {
  maxSizeMB: number;
  useWebWorker: boolean;
  initialQuality: number;
};

// optionsは必要に応じて調整する
// https://github.com/Donaldcwl/browser-image-compression#api
export const compressImage = async (
  file: File,
  options: compressImageType = {
    maxSizeMB: 20,
    useWebWorker: true,
    initialQuality: 0.85,
  }
): Promise<File> => {
  try {
    const compressedFile = await imageCompression(file, options);
    return compressedFile;
  } catch (err) {
    return Promise.reject(new Error(`compress failed: ${err}`));
  }
};

実装(utilから呼び出して使用する)

browser-image-compressionのutilを呼び出します。

import { compressImage } from "util/compress"

const compressedFile = await compressImage(file);

圧縮率(手元にあった5ファイル)

jpegの品質を85%に下げる。
平均圧縮率は76.7%くらい、ぜんぜん良さそう。

1.jpeg 3.9MB -> 948KB(圧縮率75.7%)
2.jpeg 4.8MB -> 1.1MB(圧縮率77.1%)
3.jpeg 4.5MB -> 965KB(圧縮率78.6%)
4.jpeg 3.8MB -> 914KB(圧縮率75.9%)
5.jpeg 5.0MB -> 1.2MB(圧縮率76.0%)

モバイルアプリケーション編

ライブラリの種類と選定理由

実装(util/compress.ts)

expo-image-manipulatorを使用してutilを実装します。

import {
  Action,
  ImageResult,
  manipulateAsync,
  SaveFormat,
  SaveOptions,
} from "expo-image-manipulator";

// https://docs.expo.dev/versions/latest/sdk/imagemanipulator/#imagemanipulatormanipulateasyncuri-actions-saveoptions
export const compressImage = async (
  uri: string,
  action: Action[],
  option: SaveOptions = {
    compress: 0.85,
    format: SaveFormat.JPEG,
  }
): Promise<ImageResult> => {
  try {
    const result = await manipulateAsync(uri, action, option);
    return result;
  } catch (err) {
    return Promise.reject(new Error(`compress failed: ${err}`));
  }
};

実装(utilから呼び出して使用する)

expo-image-manipulatorのutilを呼び出します。

import { compressImage } from "@util/compress"
// jpegの品質を85%に下げたうえで80%リサイズして圧縮
const compressedResult = await compressImage(photo.uri, [
{
    resize: {
        width: photo.width * 0.8,
        height: photo.height * 0.8,
    },
},
]);

圧縮率(手元にあった5ファイル)

使用しているライブラリの圧縮効率があまりよくなさそうなので、jpegの品質を85%に下げたうえで80%リサイズしている。
平均圧縮率は52.1%くらい。もっと圧縮してほしいけどまぁ及第点。

1.jpeg 3.9MB -> 2.0MB(圧縮率48.7%)
2.jpeg 4.8MB -> 2.2MB(圧縮率54.2%)
3.jpeg 4.5MB -> 2.0MB(圧縮率55.6%)
4.jpeg 3.8MB -> 1.9MB(圧縮率50.0%)
5.jpeg 5.0MB -> 2.4MB(圧縮率52.0%)


ファイル圧縮ライブラリ編

どうやって圧縮していくか方針を決める

いくつか圧縮アルゴリズムを調べましたが、gzip圧縮を採用するのが無難だろうという結論に至る。

  • 圧縮と解凍

https://bi.biopapyrus.jp/os/linux/compress.html

⇒アルゴリズムとしてはgzipが良さそう。時間もかからず圧縮率がほどほどに高いものが良い。

  • Gzip vs Bzip2 vs XZ Performance Comparison

https://www.rootusers.com/gzip-vs-bzip2-vs-xz-performance-comparison/

⇒Based on the results here, if you’re simply after being able to compress and decompress files as fast as possible with little regard to the compression ratio, then gzip is the tool for you.

ウェブアプリケーション編

ライブラリの種類と選定理由

  • fflate
    ⇒TypeScript対応しており、開発環境のブラウザ側TSでも問題なく動作したので採用。

https://zenn.dev/niccari/articles/3350ab065a48ff

  • wasm-brotli
    ⇒fflateほど人気がなさそう。
    brotliに手を出してみたい気持ちはあるが、今回はバグが少なく安牌なライブラリを採用したいので、お見送り。

https://blog.redbox.ne.jp/cdn_brotli.html

実装(util/compress.ts)

fflateを使用してutilを実装します。

import { AsyncZipOptions, decompressSync, gzipSync } from "fflate";

export const compressGzip = async (
  file: File | Blob,
  filename: string,
  compressOptions: AsyncZipOptions | undefined = undefined
): Promise<File> => {
  try {
    const options = compressOptions || {};
    const arrayBuffer: ArrayBuffer = await file.arrayBuffer();
    const fileContents = new Uint8Array(arrayBuffer);
    const zippedContent: Uint8Array = gzipSync(fileContents, options);
    return new File([zippedContent], filename);
  } catch (err) {
    return Promise.reject(new Error(`compress failed: ${err}`));
  }
};

export const deCompressGzip = async (
  file: File | Blob,
  filename: string
): Promise<File> => {
  try {
    const arrayBuffer: ArrayBuffer = await file.arrayBuffer();
    const fileContents = new Uint8Array(arrayBuffer);
    const zippedContent: Uint8Array = decompressSync(fileContents);
    return new File([zippedContent], filename);
  } catch (err) {
    return Promise.reject(new Error(`decompress failed: ${err}`));
  }
};

実装(utilから呼び出して使用する)

fflateのutilを呼び出します。

import { compressGzip, deCompressGzip } from "util/compress"

// gzip圧縮
// fileはFile型
fileName = "hogehoge.gz"
const compressedFile = await compressGzip(file, fileName);

// gzip解凍(解凍後の拡張子はファイルの形式に合わせる)
// blobはBlob型
fileName = "hogehoge.txt"
const deCompressedFile = await deCompressGzip(blob, fileName);

圧縮率(手元にあった5ファイル)

PDFとCSVを圧縮したかったので、この二つで圧縮率を検証してみます。

  • 圧縮後PDF
    PDF自体が圧縮済みのフォーマットなので、圧縮率は高いものから低いものまでレンジが広い。
    平均圧縮率は26.7%くらい。もっと圧縮してほしいけどまぁPDFなのでしょうがない。
    1.pdf 20.1MB -> 15.0MB(圧縮率25.4%)
    2.pdf 457.0KB -> 374.0KB(圧縮率18.2%)
    3.pdf 3.7MB -> 2.2MB(圧縮率40.5%)
    4.pdf 56.0KB -> 53.2KB(圧縮率5.0%)
    5.pdf 1.5MB -> 834.0KB(圧縮率44.4%)

  • 圧縮後CSV
    平均圧縮率は66.9%くらい。ぜんぜん良さそう。
    1.csv 8.1MB -> 2.6MB(圧縮率67.9%)
    2.csv 29.0KB -> 8.9KB(圧縮率69.3%)
    3.csv 534.0KB -> 128.2KB(圧縮率76.0%)
    4.csv 2.0KB -> 932.0B(圧縮率53.5%)
    5.csv 11.2MB -> 3.6MB(圧縮率67.9%)

モバイルアプリケーション編

モバイル端末でgzip圧縮する必要がなかったので、調査してません。
融通が聞かずすいません。
機会があればアップデートします。

おまけ(Python gzip圧縮/解凍)

バックエンド側でPythonを使用しているので、バックエンド側にもgzip圧縮/解凍の関数を入れておきます。

画像圧縮は解凍しなくていいですけど、gzip圧縮は解凍処理まで考えないといけないですね。引数や返り値などは自由に変更ください。

import gzip
import shutil


def ungzip_file(gz_file_path: str, write_path: str) -> None:
    """gzipファイルの解凍
    Args:
        gz_file_path (str): gzipファイルのファイルパス
        write_path (str): 解凍後の出力ファイルパス
    """
    with gzip.open(gz_file_path, mode="rb") as gzip_file:
        with open(write_path, mode="wb") as decompressed_file:
            shutil.copyfileobj(gzip_file, decompressed_file)


def gzip_file(file_path: str, gz_write_path: str) -> None:
    """ファイルのgzip圧縮
    Args:
        file_path (str): gzip圧縮を実行するファイルパス
        gz_write_path (str): gzip圧縮後の出力ファイルパス
    """
    with open(file_path, mode="rb") as file:
        with gzip.open(gz_write_path, mode="wb") as gzip_file:
            shutil.copyfileobj(file, gzip_file)

あとがき

フロントエンド技術って結構熱いですねぇ。

最近だとWebAssemblyがなぜ人気なのかいまいちわかっていなかったですが、ブラウザ側で高度な計算処理をしちゃいたいっていうのがあるんですかね。

今回圧縮ライブラリを調べたことで、WebAssemblyやってみたい!って気持ちになりました。(沼の始まり)

あと片手間フロントエンド勢のため、JavaScriptの都合があまりわかっておらず、下記のことがよくわかってないです。教えてくれる方いればお願いします!

  • Node.js用のライブラリなのか、ブラウザでも使えるライブラリなのかをどこで判断すれば良いのかわからん

  • どのようなNode.js用のライブラリでもBrowserifyとかすればブラウザ用に変換可能なのか。

読んでいただきありがとうございました。

Discussion