ARM64_Lab

Pillow 12.1.0 PNG saving took 592.98ms

In this article
  1. Using the same random image as the input
  2. Saving had a different kind of weight
  3. The LANCZOS and BILINEAR difference
  4. Why PNG looked too slow, and the measurement mistake
  5. Rotation and grayscale were straightforward
  6. How I would change the choice

With Pillow 12.1.0 and a 6000x4000 random image, the median PNG-save time was 592.98ms.

What surprised me most was not the resize itself, but that the PNG saved after shrinking to 1920px wide was heavier than the JPEG saved at 6000x4000. The side with fewer pixels was slower.

JPEG saving took 184.42ms, so PNG saving took 3.22 times as long. Writing after reducing the pixel count should look lighter, making this reversal quite counterintuitive. The measurement date was June 16, 2026. At that point I had not yet separated the interaction between the format and the input material.

The timestamp was 2026-06-16T20:18:00. The machine was a Surface Pro 11th Edition with a Snapdragon X 12-core X1E80100, running Windows 11 Pro 10.0.26200 ARM64. Python was 3.12.10 and Pillow was 12.1.0.

Using the same random image as the input

I created a 6000x4000 random image and timed each operation three times from the same input. I compared LANCZOS and BILINEAR resizing, a 90-degree rotation, grayscale conversion, JPEG saving, and PNG saving. Saving includes format compression, so it needs to be read as a different thing from a simple memory copy.

The actual processing was roughly this. The article shortens it for explanation.

from PIL import Image

img = Image.fromarray(random_rgb_6000x4000)
small_lanczos = img.resize((1920, 1280), Image.Resampling.LANCZOS)
small_bilinear = img.resize((1920, 1280), Image.Resampling.BILINEAR)
rotated = img.rotate(90, expand=True)
gray = img.convert("L")
img.save("out.jpg", quality=85)
small_lanczos.save("out.png")

The original image was 24 million pixels. Dividing the median by the original pixel count gives 9.75ns per pixel for LANCZOS, 3.92ns for BILINEAR, 3.23ns for the 90-degree rotation, and 1.84ns for grayscale conversion. This is not a fair comparison, just a rough scale for seeing which operations dominate. JPEG saving works out to 7.68ns and PNG saving to 24.71ns on the same original-image basis. PNG actually writes the resized image, so this division is not a fair comparison.

Saving had a different kind of weight

Here are all three runs. I kept runs_ms because variation disappears if you look only at the median.

Operation runs_ms median_ms
resize_lanczos_1920 215.54 / 293.73 / 233.89 233.89ms
resize_bilinear_1920 85.09 / 94.18 / 138.70 94.18ms
rotate_90 77.62 / 104.38 / 70.18 77.62ms
to_grayscale 56.06 / 44.15 / 39.20 44.15ms
save_jpeg_q85 170.00 / 196.83 / 184.42 184.42ms
save_png_1920 623.08 / 572.05 / 592.98 592.98ms

The output sizes were jpeg_bytes 18,107,176 bytes and png_bytes 6,195,646 bytes. The important condition is that JPEG remained 6000x4000 while PNG was resized to 1920x1280 first. PNG was smaller in bytes, but the resized PNG had only 2,457,600 pixels. That is about 2.52 bytes per pixel. JPEG had 24 million pixels and 18,107,176 bytes, about 0.75 bytes per pixel. The difference alone shows how difficult random noise is for PNG.

The LANCZOS and BILINEAR difference

LANCZOS took 233.89ms and BILINEAR took 94.18ms. The difference was 2.48x. LANCZOS is often the right choice for image quality, but in a process that creates many thumbnails, that difference becomes wait time directly.

For me, this was the most operationally relevant number. One or two images at 233.89ms are not noticeable. In a batch of 1,000 images, choosing LANCZOS for the same resize accumulates. If the appearance from BILINEAR at 94.18ms is acceptable, it seems worth trying first.

However, this input was a random image. Photos, screenshots, and diagrams will change both cache behavior and compression. If the goal is only to compare Pillow's resampling, the table measured separately from saving is sufficient. There is not enough here to decide about image quality and size together.

Why PNG looked too slow, and the measurement mistake

This measurement was a failure as an evaluation of PNG saving. If formats are simply listed without making clear what was held constant and what was changed, you can get an interesting reversal without getting a useful decision for the next task. That is the painful part when rereading it.

Random noise is a very bad input for PNG. PNG compresses by using nearby colors and repetition, and a random image has little relationship between neighboring pixels, so compression works poorly. png_bytes actually reached 6,195,646 bytes. At that size after resizing, it is more natural to conclude that the compressor could not find an easy pattern.

Worse, the number of pixels before saving was not the same for JPEG and PNG. JPEG stayed 6000x4000, while PNG was resized to 1920x1280 first. Looking only at time creates the interesting reversal that a small PNG is slower than a large JPEG, but that mixes the format difference with the input difference. Reading it as a comparison of formats alone is dangerous. As a comparative experiment, it was wrong. I had been reading it that way for too long before noticing.

Before seeing this mistake, I was about to turn it into a simple story that PNG is slow. Comparing jpeg_bytes and png_bytes showed that it is not reasonable to write “PNG saving is always this slow.” An image with broad areas of the same color, such as a screenshot, should produce a completely different result. Next time I would separate photos, UI screenshots, and diagrams with many flat colors.

Rotation and grayscale were straightforward

The 90-degree rotation took 77.62ms and grayscale conversion took 44.15ms. These were not surprising. Rotation rearranges pixels and grayscale converts each pixel, so they look like a straightforward contest between memory bandwidth and implementation.

Converting 24 million pixels to grayscale in 44.15ms works out to 1.84ns per pixel on the original-image basis. The 77.62ms rotation is 3.23ns. On the Surface Pro 11 Snapdragon X, that is a relatively light preprocessing step for one image. In this table, it is not the main event next to 592.98ms for PNG saving or 233.89ms for LANCZOS.

How I would change the choice

For a local script that creates many thumbnails, I would first measure the image format and resize method separately. The 2.48x difference between LANCZOS and BILINEAR helps set an acceptable visual threshold. The 592.98ms PNG-save number is striking, but avoiding PNG based on it alone would be dangerous because the random material had a major effect. I created a condition unfavorable to PNG by using random noise. I thought I was comparing formats, but I also measured how difficult the material was to compress.

Operationally, I would use JPEG as the baseline for photos and consider PNG only for UI images or material requiring transparency. For thumbnail generation, I would make drafts with BILINEAR and use LANCZOS only where needed. Even that rough two-stage approach helps as the number of images grows. Splitting by use case explains the wait better than sending every image through maximum quality.

The main takeaway was less that Pillow 12.1.0 worked normally on Windows ARM64 and more that the rough measurement design showed up directly in the numbers. Next I will repeat the test with images actually used on the site rather than random noise.

Benchmark Python
a
arm64lab — Independent publisher

Personal test notes from a Surface Pro 11th Edition with Snapdragon X Elite, used as a daily machine since May 2025. Results are based on direct measurements and do not represent any company or organization.