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:
Lycoris52
/
RustMangaReader
Windows Manga Viewer written in Rust
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.
⚡ 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.
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
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(())
}
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`);
}
}
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)