PNGs im Browser mit OxiPNG optimieren
Die PNG-Optimierung im Browser kann die Uploadgröße verringern, ohne das Bild zuerst an einen
Server zu senden. Diese Anleitung verwendet das echte Paket @jsquash/oxipng, das
OxiPNG mit WebAssembly ausführt. OxiPNG ist ein anderer Optimierer als OptiPNG; die historische
Seiten-URL behält den früheren Namen bei.
OxiPNG verstehen
OxiPNG schreibt die PNG-Komprimierung und -Repräsentation neu, ohne die decodierten Pixel zu verändern. Wir deaktivieren die Optimierung transparenter Pixel, da das Ändern unsichtbarer RGB-Werte die strikte Pixelgleichheit verletzen würde. Dateimetadaten und die binäre Repräsentation können sich ändern; verlustfreie Pixel bedeuten nicht byte-identische Dateien.
Wir verwenden den gepinnten Single-Thread-Codec des Pakets in einem dedizierten Worker. Die Benutzeroberfläche bleibt reaktionsfähig, und ein Abbruch beendet diesen Worker. So werden Cross-Origin-Isolation oder ein verschachtelter Worker-Pool nicht benötigt.
Implementierung
Erstellen Sie ein Projekt mit diesen Dateien. 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.1" }
}
Führen Sie npm install mit Node.js 24 oder neuer aus und behalten Sie die erzeugte Lockfile bei. 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 besitzt ein Job einen Worker und eine Download-URL. Die Auswahl einer Datei startet den
Job; 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 Worker für bessere Performance nutzen
Prüfen Sie in worker.js die Allokationsgrenzen, bevor Sie den eigentlichen PNG-Optimierer aufrufen.
Die Header- und Chunk-Prüfungen sind Leitplanken und kein Ersatz für die Validierung des Codecs.
Animierte PNGs werden abgelehnt, damit das Pixelbudget ein einzelnes Bild beschreibt.
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 Import von ?url ist ein Vite-Asset-Import, keine Browser-Paket-URL. Vite gibt die WASM-Datei
zusammen mit der Anwendung aus. Der Codec-Pfad ist auf diese Paketversion gepinnt; prüfen Sie ihn
bei einem Upgrade. Im Repository von jSquash finden Sie die unterstützte High-Level-API
und die Multithreading-Optionen.
Browser-Kompatibilität
Führen Sie npm run dev aus und öffnen Sie die lokale URL von Vite. Führen Sie vor der Veröffentlichung
npm run build und anschließend npm run preview aus und testen Sie auch den Produktions-Build.
Die Anwendung benötigt Modul-Worker, WebAssembly, File-/Blob-APIs und Object-URLs. Nicht
unterstützte Browser erhalten den Fehlerzustand statt einer behaupteten universellen
Versionsgarantie.
Bei der ersten Optimierung müssen die JS- und WASM-Assets der Anwendung heruntergeladen werden. Die Bilder bleiben lokal, aber das Abrufen dieser Anwendungs-Assets ist dennoch Netzwerkaktivität.
Überlegungen zur Performance
Das Beispiel akzeptiert höchstens 8 MiB, 4 Millionen Pixel und 4096 Pixel pro Seite. Es führt jeweils einen Job aus und beendet die Arbeit nach 30 Sekunden oder bei einem Abbruch. Das sind Limits der Anwendung, keine Garantien für den maximalen Speicherverbrauch des Browsers. Beginnen Sie auf Mobilgeräten konservativ.
Komprimierungsstufe 2 hält das Beispiel moderat. Höhere Stufen können länger dauern, ohne nennenswerte Einsparungen zu bringen. Wenn die erzeugte Datei größer ist, bleibt der Download das Original.
Anwendungsfälle
Dieser Ansatz kann die Uploadgröße von PNGs für Screenshots, Diagramme und transparente Assets verringern. Validieren Sie die decodierten Pixel mit repräsentativen Fixtures, einschließlich Transparenz, und prüfen Sie ungültige Dateien, Abbruch- und Wiederholungsabläufe sowie fehlende WASM-Antworten. Nutzen Sie das nicht als Beleg dafür, dass ein Upload harmlos ist: Der empfangende Server muss Dateien weiterhin validieren.
Fazit
Ein echter PNG-Optimierer im Browser benötigt einen funktionierenden Codec, begrenzte Arbeit und eine explizite Behandlung des Lebenszyklus. Diese Implementierung hält die PNG-Verarbeitung in einem verwerfbaren Worker und gibt Download-URLs wieder frei. Für die serverseitige Verarbeitung weiterer Formate können Sie Transloadits Dienst für Bildverarbeitung erkunden.
