PNGs im Browser mit Oxipng optimieren
PNG-Optimierung im Browser kann die Upload-Größe reduzieren, ohne das Bild zuvor an einen Server
zu senden. Diese Anleitung nutzt @jsquash/oxipng, das Oxipng mit WebAssembly ausführt.
Oxipng ist ein anderes Optimierungsprogramm als OptiPNG; die historische Seiten-URL behält den
früheren Namen bei.
Oxipng verstehen
Oxipng schreibt die PNG-Kompression und -Darstellung neu, ohne die decodierten Pixel zu verändern. Wir deaktivieren die Optimierung transparenter Pixel, da Änderungen unsichtbarer RGB-Werte die strikte Pixelgleichheit verletzen würden. Dateimetadaten und binäre Darstellung können sich ändern; verlustfreie Pixel bedeuten keine byteidentischen Dateien.
Wir verwenden den auf eine feste Version gesetzten Single-Thread-Codec des Pakets in einem eigenen Worker. Die Oberfläche bleibt reaktionsfähig, und ein Abbruch beendet diesen Worker. So sind weder Cross-Origin-Isolation noch ein verschachtelter Worker-Pool erforderlich.
Implementierung
Verwenden Sie ein neues, leeres Projektverzeichnis. Die Einrichtung wurde unter Linux mit
Node.js 24.15.0 und dem mitgelieferten npm 11.12.1
sowie mit Node.js 26.8.1 und npm 12.0.2 getestet. Dies sind die getesteten Versionen; spätere,
weiterhin gepflegte Node.js-24.x-Versionen eignen sich ebenfalls. Prüfen Sie
node --version und npm --version, bevor Sie die Dateien speichern.
Speichern Sie alle vier folgenden Dateien in diesem Verzeichnis. In package.json:
{
"name": "browser-png-optimizer",
"private": true,
"type": "module",
"scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" },
"dependencies": { "@jsquash/oxipng": "2.3.0" },
"devDependencies": { "vite": "7.3.6" }
}
In index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PNG optimizer</title>
</head>
<body>
<h1>PNG optimizer</h1>
<label for="file">PNG file</label>
<input id="file" type="file" accept="image/png">
<button id="cancel" type="button" disabled>Cancel</button>
<p id="status" role="status">Ready.</p>
<a id="download" hidden>Download PNG</a>
<script type="module" src="/main.js"></script>
</body>
</html>
In main.js gehören zu einem Vorgang ein Worker und eine Download-URL.
Die Auswahl einer Datei startet den Vorgang; es werden keine Bilddaten hochgeladen:
const input = document.getElementById('file')
const cancel = document.getElementById('cancel')
const status = document.getElementById('status')
const download = document.getElementById('download')
let worker
let timer
let job = 0
let downloadUrl
function release() {
worker?.terminate()
worker = undefined
clearTimeout(timer)
input.value = ''
input.disabled = false
cancel.disabled = true
}
function clearDownload() {
if (downloadUrl) URL.revokeObjectURL(downloadUrl)
downloadUrl = undefined
download.hidden = true
download.removeAttribute('href')
}
cancel.addEventListener('click', () => {
job += 1
release()
status.textContent = 'Canceled.'
})
input.addEventListener('change', async () => {
const file = input.files?.[0]
if (!file) return
const current = ++job
release()
clearDownload()
if (file.size === 0 || file.size > 8 * 1024 * 1024) {
status.textContent = 'Choose a PNG no larger than 8 MiB.'
return
}
input.disabled = true
cancel.disabled = false
status.textContent = 'Optimizing PNG.'
const fail = (message) => {
if (current !== job) return
job += 1
release()
status.textContent = message
}
timer = setTimeout(() => fail('Optimization timed out.'), 30_000)
try {
const bytes = await file.arrayBuffer()
if (current !== job) return
worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
worker.addEventListener('error', () => fail('PNG optimization failed.'))
worker.addEventListener('messageerror', () => fail('PNG optimization failed.'))
worker.addEventListener('message', ({ data }) => {
if (current !== job) return
if (!data.ok) return fail('Choose a valid, non-animated PNG within the image limits.')
const result = new Blob([data.bytes], { type: 'image/png' })
const output = result.size < file.size ? result : file
downloadUrl = URL.createObjectURL(output)
download.href = downloadUrl
download.download = 'optimized.png'
download.hidden = false
status.textContent = result.size < file.size
? 'Optimized PNG is ready.'
: 'The original PNG is already as small or smaller.'
release()
})
worker.postMessage(bytes, [bytes])
} catch {
fail('PNG optimization failed.')
}
})
window.addEventListener('pagehide', () => {
job += 1
release()
clearDownload()
})
Web Workers für bessere Leistung nutzen
Prüfen Sie in worker.js die Grenzen für die Speicherzuweisung, bevor Sie den
PNG-Optimierer aufrufen. Die Header- und Chunk-Prüfungen sind Schutzmaßnahmen, kein Ersatz für die
Validierung durch den Codec. Animierte PNGs werden abgelehnt, damit sich das Pixelbudget auf ein
einzelnes Bild bezieht.
import init, { optimise } from '@jsquash/oxipng/codec/pkg/squoosh_oxipng.js'
import wasmUrl from '@jsquash/oxipng/codec/pkg/squoosh_oxipng_bg.wasm?url'
function validatePng(buffer) {
const data = new Uint8Array(buffer)
const signature = [137, 80, 78, 71, 13, 10, 26, 10]
if (data.length < 33 || data.length > 8 * 1024 * 1024 ||
!signature.every((byte, i) => data[i] === byte)) throw new Error('Invalid PNG.')
const view = new DataView(buffer)
if (view.getUint32(8) !== 13 || view.getUint32(12) !== 0x49484452) {
throw new Error('Missing PNG header.')
}
const width = view.getUint32(16)
const height = view.getUint32(20)
if (width === 0 || height === 0 || width > 4096 || height > 4096 ||
width * height > 4_000_000) throw new Error('Image exceeds pixel limit.')
let offset = 8
while (offset + 12 <= data.length) {
const length = view.getUint32(offset)
const type = view.getUint32(offset + 4)
if (length > data.length - offset - 12 || type === 0x6163544c) {
throw new Error('Invalid or animated PNG.')
}
offset += length + 12
if (type === 0x49454e44) {
if (length !== 0 || offset !== data.length) throw new Error('Invalid PNG ending.')
return
}
}
throw new Error('Truncated PNG.')
}
self.addEventListener('message', async ({ data }) => {
try {
if (!(data instanceof ArrayBuffer)) throw new Error('Invalid input.')
validatePng(data)
const response = await fetch(wasmUrl)
if (!response.ok) throw new Error('WASM unavailable.')
await init(await response.arrayBuffer())
const bytes = optimise(new Uint8Array(data), 2, false, false)
self.postMessage({ ok: true, bytes: bytes.buffer }, [bytes.buffer])
} catch {
self.postMessage({ ok: false })
}
})
Der Low-Level-Aufruf von optimise
übergibt Stufe 2, deaktiviert Interlacing und lässt transparente RGB-Werte unverändert.
Das Suffix ?url ist ein
Asset-Import von Vite. Vite gibt die WASM-Datei zusammen mit der
Anwendung aus. Der Codec-Pfad ist an diese Paketversion gebunden; prüfen Sie ihn bei einem Upgrade.
Die Paketdokumentation von jSquash beschreibt die High-Level-API
und die Multithreading-Optionen.
Browserkompatibilität
Installieren Sie im Projektverzeichnis die Pakete in den festgelegten Versionen und starten Sie die App:
npm install && npm run dev
Behalten Sie die generierte Datei package-lock.json bei. Falls die Installation
fehlschlägt, beheben Sie den gemeldeten Abhängigkeits- oder Netzwerkfehler und wiederholen Sie
diesen Befehl im selben Verzeichnis; die gespeicherten Quelldateien bleiben erhalten.
Öffnen Sie die von Vite ausgegebene lokale URL in einem Browser mit Unterstützung für
Modul-Worker, WebAssembly, File/Blob-APIs und Objekt-URLs. Der gesamte Ablauf wurde in Chromium 145
unter Linux getestet; andere Browser-Engines und Mobilgeräte müssen separat geprüft werden.
Wählen Sie mit PNG file eine Datei aus.
Der Status ändert sich zu Optimized PNG is ready.
Klicken Sie auf Download PNG, um
optimized.png zu speichern. Ist die optimierte Datei nicht kleiner, erscheint
The original PNG is already as small or smaller.
Der Download enthält dann die ursprünglichen Bytes.
Beenden Sie den Entwicklungsserver mit Ctrl+C. Erstellen Sie den Build und testen Sie denselben Ablauf aus Dateiauswahl und Download mit den Produktions-Assets:
npm run build && npm run preview
Öffnen Sie die von Vite ausgegebene Vorschau-URL und beenden Sie den Server anschließend mit Ctrl+C. Vite preview stellt den lokalen Build zur Prüfung bereit; verwenden Sie für die Bereitstellung einen statischen Host.
Beim Öffnen der App wird ihr JS abgerufen; der erste Vorgang ruft zusätzlich das WASM-Asset ab. Bilder bleiben lokal, doch das Abrufen dieser Anwendungs-Assets ist weiterhin Netzwerkaktivität.
Hinweise zur Leistung
Das Beispiel akzeptiert höchstens 8 MiB, 4 Millionen Pixel und 4096 Pixel pro Seite. Es führt jeweils einen Vorgang aus und beendet die Verarbeitung nach 30 Sekunden oder bei einem Abbruch. Dies sind Anwendungslimits, keine Garantien für den maximalen Speicherbedarf des Browsers. Beginnen Sie auf Mobilgeräten mit vorsichtigen Grenzen.
Kompressionsstufe 2 hält den Aufwand im Beispiel moderat. Höhere Stufen können länger dauern, ohne die Dateigröße nennenswert zu reduzieren. Ist die erzeugte Datei größer, bleibt der Download das Original.
Abbrechen und erneut versuchen
Klicken Sie auf Cancel, solange die Verarbeitung noch aussteht. Dadurch wird der Worker beendet und der Status ändert sich zu Canceled. Sie können dieselbe Datei erneut oder eine andere Datei auswählen. Beim Start eines Vorgangs wird der vorherige Download entfernt; ein abgebrochener oder fehlgeschlagener Vorgang hat kein Ergebnis zum Herunterladen.
Eine leere Datei oder eine Datei mit mehr als 8 MiB wird sofort abgelehnt. Der Worker lehnt ungültige Header, animierte PNGs und Bilder außerhalb der Abmessungs- oder Pixellimits ab, bevor er den Codec lädt. Der Codec prüft die verbleibenden PNG-Daten. Nach 30 Sekunden wird ein festhängender Vorgang beendet und der Status lautet Optimization timed out. Sie können erneut eine Datei auswählen, um es noch einmal zu versuchen.
Falls ein nachweislich gültiges PNG fehlschlägt, prüfen Sie im Netzwerkbereich der
Browser-Entwicklertools, ob der Abruf des Assets .wasm erfolgreich ist.
Ein fehlendes Asset führt ebenso wie ein Codec-Fehler dazu, dass kein Download bereitsteht.
Erstellen Sie den Build erneut mit den festgelegten Paketversionen und testen Sie die Vorschau
noch einmal. Bilder bleiben in diesem Beispiel lokal; ein Server, der einen späteren Upload
empfängt, muss die Datei dennoch validieren.
Für die serverseitige Verarbeitung weiterer Formate entdecken Sie den Service zur Bildverarbeitung von Transloadit.
