MENU

Rust vs Python vs Node.js ― ZIP内画像読み込み速度を比較してみた

2026 4/05

あらすじ

以前、Rust で漫画ビューアアプリ「RustMangaReader」を作った時、
読み込む速度が一番重要視しているので、どの言語が一番早いか試しました。

漫画ビューアの方に興味ある方はこちら

zenn.devhttps://zenn.dev/lycoris52/articles/960e3e840d2cb1zenn.dev

せっかくなので、その時のベンチマークをシェアしようと思います。


言語前提する時の条件

ですが、後で AI 機能拡張などするならこれでもいいかなーと

  • C++ 多分一番早いですが、書きにくすぎる
  • Python ものすごい書きやすいし、今の仕事でずっと使っていますが、遅い。。。
  • Javascript 他の OS に展開しやすいのが魅力。Native のライブラリーも多いので、遅くないでは?と期待しました。
  • Rust これが前からずっと気になって勉強してみたいと思っていましたが、触ったことがないので本当に早いか?と疑問が残っている

なので、各言語を簡単なプログラムを書いて、速度計ることにしました。
C++ は面倒そうなので、書いてなかった。。。すみませんでした。


準備

作ろうとした漫画ビューアアプリの主な機能は ZIP ファイルから開封せず画像直接読み込むなので、それをプログラムにしました。

画像がいっぱい入っている ZIP ファイルが必要になるので、用意した。
ChatGPT に適当な高解像度画像(4k)を生成して 10 枚複製して、ZIP に入れました。

画像1枚のサイズは 7.17MB で ZIP ファイルは 71.7MB です。


マシンスペック

ベンチマークするマシンのスペックはこんな感じ

CPU : AMD Ryzen 9 7900
RAM : G.Skill F56000J3036G 32G DDR5x2
NVME : SAMSUNG 990 Pro 4TB

各言語のコード

別にコード見たくない方は結果まで飛ばせます。

Rust

use std::env;
use std::fs::File;
use std::io::{Read, Seek};
use std::time::Instant;

fn parse_arg(args: &[String], key: &str, default: &str) -> String {
    args.iter()
        .position(|a| a == key)
        .and_then(|i| args.get(i + 1))
        .cloned()
        .unwrap_or_else(|| default.to_string())
}

fn main() -> anyhow::Result<()> {
    let zip_path = "./benchmark_images.zip";
    let iters: usize = 10;

    // Warmup + timed iterations
    let mut best_ms = f64::INFINITY;
    let mut last_stats = (0usize, 0u64);

    for iter in 0..iters {
        let file = File::open(&zip_path)?;
        let mut archive = zip::ZipArchive::new(file)?;

        let start = Instant::now();

        let mut count = 0usize;
        let mut total_bytes: u64 = 0;

        // Iterate entries in zip
        for i in 0..archive.len() {
            let mut f = archive.by_index(i)?;
            let name = f.name().to_string();

            let mut buf = Vec::with_capacity(f.size() as usize);
            f.read_to_end(&mut buf)?;
            total_bytes += buf.len() as u64;

            // Decode to pixels (forces actual image parsing)
            let img = image::load_from_memory(&buf)?;
            // Force pixel materialization
            let _rgba = img.to_rgba8();

            count += 1;
        }

        let elapsed = start.elapsed().as_secs_f64() * 1000.0;
        last_stats = (count, total_bytes);

        // skip first run as warmup-ish if you want, but we’ll just keep best
        if elapsed < best_ms {
            best_ms = elapsed;
        }

        eprintln!("iter {}: {:.2} ms", iter + 1, elapsed);
    }

    let (count, total_bytes) = last_stats;
    let secs = best_ms / 1000.0;
    let mb = total_bytes as f64 / (1024.0 * 1024.0);

    println!("zip: {}", zip_path);
    println!("images: {}", count);
    println!("bytes read: {} ({:.2} MiB)", total_bytes, mb);
    println!("best time: {:.2} ms", best_ms);
    if secs > 0.0 {
        println!("throughput: {:.2} images/s", count as f64 / secs);
        println!("throughput: {:.2} MiB/s", mb / secs);
    }

    Ok(())
}

Python

import time
import zipfile
from io import BytesIO
from PIL import Image

def run_once(zip_path: str) -> tuple[float, int, int]:
    t0 = time.perf_counter()

    count = 0
    total_bytes = 0

    with zipfile.ZipFile(zip_path, "r") as zf:
        for info in zf.infolist():
            data = zf.read(info)  # read entry into memory (no extracting)
            total_bytes += len(data)

            # Decode image fully (force pixel load)
            with Image.open(BytesIO(data)) as im:
                im.load()

            count += 1

    t1 = time.perf_counter()
    return (t1 - t0), count, total_bytes

best = float("inf")
last = (0, 0)

for i in range(10):
    sec, count, total_bytes = run_once("benchmark_images.zip")
    last = (count, total_bytes)
    best = min(best, sec)
    print(f"iter {i+1}: {sec*1000:.2f} ms")

count, total_bytes = last
mib = total_bytes / (1024 * 1024)

print(f"zip: benchmark_images.zip")
print(f"images: {count}")
print(f"bytes read: {total_bytes} ({mib:.2f} MiB)")
print(f"best time: {best*1000:.2f} ms")
print(f"throughput: {count/best:.2f} images/s")
print(f"throughput: {mib/best:.2f} MiB/s")

Javascript (Node.js)

import fs from "node:fs";
import yauzl from "yauzl";
import sharp from "sharp";

function hrNowSec() {
    return Number(process.hrtime.bigint()) / 1e9;
}

async function readEntryToBuffer(zipFile, entry) {
    return new Promise((resolve, reject) => {
        zipFile.openReadStream(entry, (err, stream) => {
            if (err) return reject(err);
            const chunks = [];
            let total = 0;
            stream.on("data", (c) => { chunks.push(c); total += c.length; });
            stream.on("end", () => resolve({ buf: Buffer.concat(chunks, total), bytes: total }));
            stream.on("error", reject);
        });
    });
}

async function runOnce(zipPath, mode) {
    const t0 = hrNowSec();

    let count = 0;
    let totalBytes = 0;

    const zipFile = await new Promise((resolve, reject) => {
        yauzl.open(zipPath, { lazyEntries: true }, (err, zf) => {
            if (err) return reject(err);
            resolve(zf);
        });
    });

    const done = new Promise((resolve, reject) => {
        zipFile.readEntry();

        zipFile.on("entry", async (entry) => {
            const { buf, bytes } = await readEntryToBuffer(zipFile, entry);
            totalBytes += bytes;

            // Force actual decode to pixels
            // raw().toBuffer() makes sharp decode the image data
            await sharp(buf).raw().toBuffer();

            count += 1;
            zipFile.readEntry();
        });

        zipFile.on("end", () => resolve());
        zipFile.on("error", reject);
    });

    await done;
    zipFile.close();

    const t1 = hrNowSec();
    return { sec: (t1 - t0), count, totalBytes };
}

async function main() {
    const zip = "benchmark_images.zip"
    const iters = 10;

    let best = Number.POSITIVE_INFINITY;
    let last = { count: 0, totalBytes: 0 };

    for (let i = 0; i < iters; i++) {
        const r = await runOnce(zip);
        last = r;
        best = Math.min(best, r.sec);
        console.log(`iter ${i + 1}: ${(r.sec * 1000).toFixed(2)} ms`);
    }

    const mib = last.totalBytes / (1024 * 1024);
    console.log(`zip: ${zip}`);
    console.log(`images: ${last.count}`);
    console.log(`bytes read: ${last.totalBytes} (${mib.toFixed(2)} MiB)`);
    console.log(`best time: ${(best * 1000).toFixed(2)} ms`);
    if (best > 0) {
        console.log(`throughput: ${(last.count / best).toFixed(2)} images/s`);
        console.log(`throughput: ${(mib / best).toFixed(2)} MiB/s`);
    }
}

ベンチマーク結果

-RustPythonJavascript
iter 1557.73 ms1056.06 ms811.30 ms
iter 2556.50 ms1034.25 ms819.17 ms
iter 3555.70 ms1036.28 ms803.66 ms
iter 4556.82 ms1034.54 ms813.86 ms
iter 5555.07 ms1035.30 ms816.00 ms
iter 6557.37 ms1036.74 ms793.65 ms
iter 7556.83 ms1033.83 ms792.46 ms
iter 8555.69 ms1035.08 ms774.49 ms
iter 9564.42 ms1035.40 ms771.28 ms
iter 10554.54 ms1034.46 ms734.43 ms
bytes read75252870 (71.77 MiB)75252870 (71.77 MiB)75252870 (71.77 MiB)
best time554.54 ms1033.83 ms734.43 ms
throughput18.03 images/s9.67 images/s13.62 images/s
throughput129.42 MiB/s69.42 MiB/s97.72 MiB/s

やっぱ想定通りに、Rust が一番早かった
と思ってましたが、Javascript もぶっちゃけ悪くないですね。

そして、Python ですが、 AI 翻訳など追加する場合、別途で API 作るかなーと思います。
さすがに Python でこのタスクは遅すぎますね。

なかなか面白くない結果ですが、ご参考になれば嬉しいです。

この記事を書いた人

Full stack developer working on Game programming, AI, Data Analysis, and various server backend tech. Feel free to contact me via Huggingface or Linkedin.

コメント

コメントする

目次
閉じる