ReactNativeで画像サイズを小さくし、アプリを軽量化する方法【webp】

私が開発しているLangJournalでは画像を全てwebpにしています。劣化はほとんどなく70%ほど画像サイズを小さくできます。

実装に必要なこと

Expo使っている場合

修正不要

Expo使っていない場合

Androidのみ回収が必要です。

android/app/build.gradle
dependencies {

  // For WebP support, including animated WebP
  implementation 'com.facebook.fresco:animated-webp:2.5.0'
  implementation 'com.facebook.fresco:webpsupport:2.5.0'

  // For WebP support, without animations
  implementation 'com.facebook.fresco:webpsupport:2.5.0'
}

参考
https://reactnative.dev/docs/image#gif-and-webp-support-on-android

webp画像を一括で作る方法

以下は補足ですが、すでにpngなどで運用されている方がwebpに一括で変える方法を記載しておきます。

私も最初は全てpngで運用していたのですが、規模が大きくなったタイミングでwebpに変えました。一つずつ変換するのは手間だったので、sharpという画像圧縮ライブラリを使いました。以下が実際のソースコードです。

こちらはReactNativeとは別プロジェクトでやっております。

node.js
/**
 * 画像一括変換スクリプト
 * 参考: https://sharp.pixelplumbing.com/
 *
 * インストール
 * npm install --save-dev sharp glob path fs-extra svgo zlib commander
 *
 * 実行(例)
 * node convertImage.mjs -i ./inputs -o ./outputs -w -t
 *
 * ヘルプ
 * node convertImage.mjs -h
 */
import sharp from "sharp";
import {globSync} from "glob";
import path from "path";
import fse from "fs-extra";
import { Command } from "commander";
import { optimize, loadConfig } from "svgo";
import zlib from "zlib";

// 引数設定
const program = new Command();
program
  .requiredOption("-i, --input <string>", "ソースディレクトリ(必須)")
  .requiredOption("-o, --out <string>", "出力先ディレクトリ(必須)")
  .option("-m, --minify", "画像の最適化を行う(同一拡張子での変換)", false)
  .option("-w, --webp", "webp化を行う", false)
  .option("-a, --webp-suffix-add", "webp化の際、拡張子を書き換え(false)するか追加(true)するか", false)
  .option("-v, --svg", "svgの最適化を行う", false)
  .option("-z, --svgz", "svgzを出力する", false)
  .option("-n, --nosvg", "svgzを出力した場合、svgは出力しない", false)
  .option("-t, --truncate", "出力先のディレクトリを空にする", false)
  .parse();

/**
 * 設定項目ここから
 */
// 変換対象拡張子とエンコーダーの設定
const GET_ENCODER_FROM_EXTENSION = {
  jpg: "jpg",
  jpeg: "jpg",
  png: "png"
};

// 変換オプション(参考: https://sharp.pixelplumbing.com/api-output)
const ENCODER_OPTIONS = {
  png: {
    compressionLevel: 9,
    adaptiveFiltering: true,
    progressive: true
  },
  jpg: {
    quality: 90
  },
  webp: {
    png: {
      lossless: true
    },
    jpg: {
      quality: 90
    }
  }
};

// SVGを認識する拡張子
const SVG_EXTENSION = "svg";

/**
 * 設定項目ここまで
 */

// オプション項目読み取り
const Options = program.opts();
const IMAGE_DIR = Options.input;
const OUTPUT_DIR = Options.out;
const DO_OPTIMIZE = Options.minify;
const DO_OPTIMIZE_SVG = Options.svg;
const ENCODE_WEBP = Options.webp;
const WEBP_SUFFIX_ADD = Options.webpSuffixAdd;
const ENCODE_SVGZ = Options.svgz;
const NO_SVG = ENCODE_SVGZ && Options.nosvg;
const TRUNCATE_BEFORE = Options.truncate;
const svgoConfig = await loadConfig(); // svgo.config.jsから設定を取得

// ソースディレクトリからファイル一覧を取得
let imageFileList = [];
globSync(IMAGE_DIR + "/**/*.*").map(function(file) {
  // windows対応
  file = "./" + file.replace(/\\/g, "/");
  imageFileList.push(file.replace(IMAGE_DIR, "."));
});

// 出力先ディレクトリを空にする
if (TRUNCATE_BEFORE) {
  fse.emptyDirSync(OUTPUT_DIR);
}

// 変数初期化
const ts_start = Date.now();
let ts_worker_start = Date.now();
let ts_worker_end;
let targetFileNum = imageFileList.length;
let encodedFileNum = 1;

await Promise.all(
  imageFileList.map(async imagePath => {
    // ファイルの拡張子を取得
    const fileExtension = path.extname(imagePath).substring(1).toLowerCase();
    // ソースパスと出力パスを取得
    const sourcePath = path.join(IMAGE_DIR, imagePath);
    const destinationPath = path.join(OUTPUT_DIR, imagePath);

    // destinationPathのディレクトリがなければ作成
    await fse.ensureDir(path.dirname(destinationPath))

    // 拡張子からエンコーダーを取得
    const encoder = GET_ENCODER_FROM_EXTENSION[fileExtension];
    // SVGかどうか
    const isSvg = fileExtension === "svg";

    // 変数の初期化
    let action = "";
    let isCopy = !encoder && !isSvg;
    let encodeOptions = {};
    let binaryData = "";

    if (encoder !== "") {
      // エンコーダーの設定
      if (DO_OPTIMIZE) {
        encodeOptions[encoder] = ENCODER_OPTIONS[encoder];
      }
      if (ENCODE_WEBP) {
        encodeOptions["webp"] = ENCODER_OPTIONS["webp"];
      }
      if (Object.keys(encodeOptions).length === 0) {
        isCopy = true;
      }
    }

    if (isCopy) {
      // エンコード対象外
      await fse.copy(sourcePath, destinationPath);
      action = "copied";
    } else if (isSvg) {
      // SVGの処理
      binaryData = fse.readFileSync(sourcePath);
      if (DO_OPTIMIZE_SVG) {
        binaryData = optimize(binaryData, svgoConfig);
        binaryData = binaryData.data;
      }
      if (!NO_SVG) {
        await fse.outputFile(destinationPath, binaryData);
        action += "optimized";
      }
      if (ENCODE_SVGZ) {
        await zlib.gzip(binaryData, async (__, svgzData) => {
          await fse.outputFile(destinationPath + "z", svgzData);
        });
        if (action !== "") {
          action += " and ";
        }
        action += "encoded to svgz";
      }
    } else {
      // 最適化を行う
      if (DO_OPTIMIZE) {
        // encoder と encodeOptions を指定して最適化
        await sharp(sourcePath)
          .toFormat(encoder, ENCODER_OPTIONS[encoder])
          .toFile(destinationPath);
        action += "optimized";
      }
      if (ENCODE_WEBP) {
        // webp と encodeOptions を指定して最適化
        const destinationPathWebp = WEBP_SUFFIX_ADD
          ? destinationPath + ".webp"
          : destinationPath.slice(0, fileExtension.length * -1) + "webp";

        await sharp(sourcePath)
          .webp(ENCODER_OPTIONS["webp"][encoder])
          .toFile(destinationPathWebp);
        if (action !== "") {
          action += " and ";
        }
        action += "encoded to webp";
      }
    }

    // 変換結果表示
    ts_worker_end = Date.now();
    console.info(
      "[",
      encodedFileNum++,
      "/",
      targetFileNum,
      "]",
      imagePath,
      "is",
      action,
      "(",
      ts_worker_end - ts_worker_start,
      "ms",
      ")"
    );
    ts_worker_start = ts_worker_end;
  })
);

// 結果表示
console.info("done!", "(", "total:", ts_worker_end - ts_start, "ms", ")");

私はinputsフォルダに変換したい画像を入れて、outputsフォルダに吐き出すようにしております。

参考記事
https://qiita.com/bananacoffee/items/d7a4b5cb4afff7efd162

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です