DEV Community

Lycoris52
Lycoris52

Posted on

Rust vs Python vs Node.js Speed Comparison on ZIP Image Loading Benchmark

When I previously created a manga viewer application in Rust called RustMangaReader, the most important thing for considering which programming language to be used was loading speed.

So I need to test which programming language is the fastest.

If you are interested in the manga viewer itself, it's on this github:

GitHub logo Lycoris52 / RustMangaReader

Windows Manga Viewer written in Rust

日本語の説明はこちら

An example image RustMangaReader An example image

RustMangaReader is a high-performance, lightweight offline manga and comic viewer built in Rust.
Designed specifically for the Windows, it focuses on providing a fluid, lag-free reading experience through preloading and native rendering.

app image

⚡ Key Features

  • Built for Speed: Uses a dual-buffer system to preload upcoming and previous pages in the background, ensuring near-instant page turns.
  • Optimized for Windows:
    • Leverages Windows-native sorting (so "Page2" comes before "Page10")
    • High-performance GPU rendering.
    • No Zip extraction required RustMangaReader reads directly from compressed files saving disk space without sacrificing speed.
  • Smart Scaling: Includes multiple resampling algorithms from Nearest Neighbor to Lanczos3 to make every scan look its best on your monitor.
  • Tailored Reading: Supports Single Page, Double Page (Left-to-Right), and Double Page (Right-to-Left) modes, including a "Cover + Spreads" shift toggle (Odd/Even page).

🎁 Free & Open Feedback

MangaReader is a completely free application!
I want to make it…

Since I already ran some benchmarks before developing it, I thought it might be helpful to share the results.


Choosing the Languages

Here is my speculation about each language before testing:

C++
Probably the fastest, but honestly it is really painful to write.

Python
Very easy to write, and I use it a lot at work.
But it is slow.
Still, if I want to add AI features later, it will be really useful if the base program is developed using python.

JavaScript
Very easy to deploy on many operating systems.
There are many native libraries, so I expected it might not be too slow.

Rust
I have been curious about Rust for a long time and wanted to learn it.
But since I had never used it before, I wasn’t sure if it was really that fast.

So I decided to write simple programs in each language and measure the speed.

(I didn’t write the C++ version because it looks too troublesome… sorry.)


Preparation

The main feature I wanted for the manga viewer was:

Reading images directly from a ZIP file without extracting it.
Enter fullscreen mode Exit fullscreen mode

So the benchmark program does exactly that.

Now, I needed a ZIP file containing many images, so I created one.
I asked ChatGPT to generate a random 4K image, duplicated it 10 times, and put them into a ZIP file.

Size of one image: 7.17 MB
ZIP file size: 71.7 MB


Machine Specifications

The benchmark machine specs:

CPU  : AMD Ryzen 9 7900
RAM  : G.Skill F56000J3036G 32GB DDR5x2
NVMe : Samsung 990 Pro 4TB
Enter fullscreen mode Exit fullscreen mode

Benchmark Code

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(())
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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`);
    }
}
Enter fullscreen mode Exit fullscreen mode

The programs all do the same thing:

  • Open the ZIP file
  • Read each image entry into memory
  • Decode the image

Each test runs 10 iterations, and the best time is used as the result.


Benchmark Results

- Rust Python Javascript
iter 1 557.73 ms 1056.06 ms 811.30 ms
iter 2 556.50 ms 1034.25 ms 819.17 ms
iter 3 555.70 ms 1036.28 ms 803.66 ms
iter 4 556.82 ms 1034.54 ms 813.86 ms
iter 5 555.07 ms 1035.30 ms 816.00 ms
iter 6 557.37 ms 1036.74 ms 793.65 ms
iter 7 556.83 ms 1033.83 ms 792.46 ms
iter 8 555.69 ms 1035.08 ms 774.49 ms
iter 9 564.42 ms 1035.40 ms 771.28 ms
iter 10 554.54 ms 1034.46 ms 734.43 ms
bytes read 75252870 (71.77 MiB) 75252870 (71.77 MiB) 75252870 (71.77 MiB)
best time 554.54 ms 1033.83 ms 734.43 ms
throughput 18.03 images/s 9.67 images/s 13.62 images/s
throughput 129.42 MiB/s 69.42 MiB/s 97.72 MiB/s

Conclusion

As expected, Rust was the fastest.
However, JavaScript was actually not bad at all.

As for Python, if I want to add AI translation features later, I might implement them as a separate API server instead.
For this kind of task, Python is simply too slow.

The results were not very surprising, but I hope they are useful for someone.

Top comments (0)