So erstellen Sie Bild-Thumbnails vor dem Upload
Erstellen Sie Bildthumbnails vor dem Upload mit FileReader-Vorschau, Canvas-toBlob-Resize, FormData-Upload und passenden multer-Feldern.
Eine Vorschau zeigt die vom Benutzer ausgewählte Datei in kleinerer Darstellung an, lädt jedoch exakt dieselben Bytes hoch; ein Thumbnail ist dagegen ein tatsächlich neu kodiertes, kleineres Bild, das Sie zusätzlich zum Original – oder anstelle davon – hochladen können.
Wenn Sie schon einmal eine vermeintliche Thumbnail-Funktion ausgeliefert und dann zugesehen haben, wie sich ein 12 MB großes Handyfoto trotzdem zum Server hochquält, wissen Sie bereits, dass dieser Unterschied entscheidend ist. Die meisten Tutorials verwischen diese Abgrenzung und nennen ein herunterskaliertes <img> ein „Thumbnail” – was schlicht falsch ist: Ein Bild per CSS auf dem Bildschirm zu verkleinern, ändert nichts an der übertragenen Datenmenge. Dieser Artikel trennt beide Ansätze sauber und führt von der einfachsten FileReader-Vorschau bis hin zu einem echten, per Canvas skalierten Thumbnail, das Sie hochladen können – samt der Fallstricke bei Aufräumarbeiten und Validierung, an denen Upload-UIs in der Produktion regelmäßig scheitern.
Die wichtigsten Erkenntnisse
- Eine Vorschau via
FileReader.readAsDataURLdient rein der Anzeige: Sie lädt exakt die ursprünglichen Bytes hoch und reduziert die übertragene Datenmenge in keiner Weise. - Um eine kleinere Datei zu erzeugen, müssen Sie das Bild auf ein
<canvas>neu zeichnen und es mitcanvas.toBlob(callback, 'image/jpeg', 0.7)neu kodieren. canvas.toBlob()arbeitet asynchron und übergibt den Blob an einen Callback, statt ihn zurückzugeben – kapseln Sie den Aufruf daher in ein Promise, um das Ergebnis mitawaitabzuwarten.- Laden Sie beide Dateien in einem einzigen
FormDatahoch (das originaleFileund den Thumbnail-Blobmit explizitem Dateinamen) und nehmen Sie sie serverseitig mit multersupload.fields()entgegen. - Zu jedem
URL.createObjectURL()gehört einURL.revokeObjectURL(), andernfalls verbleibt die zugrunde liegende Datei im Speicher, bis das Dokument entladen wird.
Vorschau vs. echtes Thumbnail: Was brauchen Sie?
Klären Sie das, bevor Sie eine Zeile Code schreiben, denn die beiden Wege teilen praktisch keine gemeinsame Logik. Eine Vorschau bestätigt visuell, dass die richtige Datei ausgewählt wurde. Ein Thumbnail ist ein neues, kleineres Bild-Asset (weniger Bytes, kleinere Abmessungen), das die Upload-Zeit und die serverseitige Verarbeitung reduziert und sich als Rasterbild speichern lässt, ohne das Original erneut verarbeiten zu müssen.
| Ziel | Technik | Erzeugt eine neue, kleinere Datei? | Einsatz, wenn |
|---|---|---|---|
| Ausgewähltes Bild sofort anzeigen | FileReader oder URL.createObjectURL(file) | Nein | Sie lediglich eine visuelle Bestätigung benötigen |
| Hochgeladene Bytes reduzieren / kleine Variante speichern | Canvas + toBlob() | Ja | Sie zu einem Server oder CDN hochladen |
| Drag & Drop, Fortschritt, mehrere Größen, Validierung | Bibliothek (FilePond) | Ja | Sie eine Rundum-sorglos-Lösung wollen |
Eine FileReader-Vorschau mit readAsDataURL verkleinert die übertragene Datenmenge in keiner Weise. Um die hochgeladenen Bytes tatsächlich zu reduzieren, müssen Sie das Bild auf ein <canvas> neu zeichnen und mit canvas.toBlob() neu kodieren.
Wie erstellt man eine Bildvorschau mit FileReader?
Discover how at OpenReplay.com.
Für eine reine Anzeigevorschau lauschen Sie auf das change-Event des File-Inputs, lesen die Datei mit FileReader.readAsDataURL und weisen die resultierende Data-URL in reader.onload einem <img> zu. Das ist die schnellste umsetzbare Lösung – und sie skaliert die Datei nicht.
<input type="file" id="fileInput" accept="image/*" multiple>
<div id="previews"></div>
const input = document.getElementById('fileInput');
const previews = document.getElementById('previews');
input.addEventListener('change', (e) => {
previews.innerHTML = '';
Array.from(e.target.files).forEach((file) => {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (ev) => {
const img = new Image();
img.src = ev.target.result; // base64 data URL
img.alt = `Preview of ${file.name}`;
previews.appendChild(img);
};
reader.readAsDataURL(file);
});
});
Beachten Sie das Muster Array.from(files).forEach(...). Das verbreitete jQuery-Tutorial für mehrere Dateien verwendet eine einzige reader-Variable innerhalb einer for-Schleife wieder, sodass jedes onload die jeweils letzte Datei einschließt: ein Closure-Bug, der wiederholt dasselbe Bild anzeigt. Jede Datei benötigt innerhalb der Iteration ihren eigenen FileReader – und genau das liefert forEach kostenlos mit.
Eine ressourcenschonendere Alternative für große Dateien ist URL.createObjectURL(file). Die Methode gibt eine kurze Blob-URL statt eines Base64-Strings zurück und vermeidet so den Speicher-Overhead, der beim Kodieren der gesamten Datei in eine Data-URL entsteht – um den Preis eines zwingend erforderlichen revokeObjectURL() zu einem späteren Zeitpunkt.
Wie erzeugt man ein skaliertes Thumbnail mit Canvas?
Um tatsächlich eine kleinere Datei zu erzeugen, laden Sie das Bild, skalieren seine Abmessungen unter Beibehaltung des Seitenverhältnisses, zeichnen es auf ein Canvas und kodieren es neu. Um das Seitenverhältnis zu wahren, skalieren Sie beide Dimensionen mit demselben Faktor (maxSize / longestSide), statt Breite und Höhe unabhängig voneinander zu setzen.
canvas.toBlob(callback, 'image/jpeg', 0.7) ist asynchron: Die Methode übergibt den Blob an ihren Callback, statt ihn zurückzugeben – kapseln Sie sie also in ein Promise, wenn Sie das Thumbnail mit await abwarten möchten. Ein reguläres Canvas bietet keine Promise-basierte Variante; nur OffscreenCanvas.convertToBlob() gibt nativ ein Promise zurück.
function canvasToBlob(canvas, type, quality) {
return new Promise((resolve, reject) => {
canvas.toBlob(
(blob) => (blob ? resolve(blob) : reject(new Error('toBlob failed'))),
type,
quality
);
});
}
async function makeThumbnail(file, maxSize = 200) {
const url = URL.createObjectURL(file);
try {
const img = await new Promise((res, rej) => {
const i = new Image();
i.onload = () => res(i);
i.onerror = rej;
i.src = url;
});
const scale = Math.min(1, maxSize / Math.max(img.width, img.height));
const w = Math.round(img.width * scale);
const h = Math.round(img.height * scale);
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
return await canvasToBlob(canvas, 'image/jpeg', 0.7);
} finally {
URL.revokeObjectURL(url);
}
}
Das dritte Argument von toBlob legt die Kodierungsqualität auf einer Skala von 0 bis 1 fest. Nur verlustbehaftete Formate werten es aus, es verändert also die Ausgabe für image/jpeg und image/webp und bleibt bei image/png wirkungslos. Ein Wert zwischen 0,6 und 0,8 hat sich in der Praxis als optimal erwiesen. Ein Mehrmegapixel-Foto auf ein 200px-JPEG neu zu kodieren, reduziert dessen Größe typischerweise um ein bis zwei Größenordnungen – genau darin liegt der Sinn, dies clientseitig zu erledigen.
Thumbnail anzeigen und beide Dateien hochladen
Zeigen Sie den erzeugten Blob mit URL.createObjectURL(blob) an und senden Sie ihn anschließend gemeinsam mit dem Original an den Server. Für den Upload hängen Sie beide Dateien an ein einziges FormData an: das originale File und den Thumbnail-Blob, wobei Sie einen Dateinamen als drittes Argument übergeben. Senden Sie das per fetch als POST; auf dem Server nimmt multers upload.fields() die beiden Felder getrennt entgegen.
async function upload(file) {
const thumb = await makeThumbnail(file);
const preview = new Image();
preview.src = URL.createObjectURL(thumb); // remember to revoke later
document.body.appendChild(preview);
const form = new FormData();
form.append('originalFiles', file, file.name);
form.append('thumbnails', thumb, `thumb-${file.name}.jpg`);
await fetch('/api/upload', { method: 'POST', body: form });
}
Im Backend parst multer den Multipart-Body. Die Feldnamen in upload.fields() müssen exakt mit den FormData.append-Schlüsseln übereinstimmen, andernfalls wird das betreffende Feld stillschweigend verworfen:
const upload = multer({ dest: 'uploads/' });
app.post('/api/upload', upload.fields([
{ name: 'originalFiles', maxCount: 10 },
{ name: 'thumbnails', maxCount: 10 },
]), (req, res) => res.json({ ok: true }));
Die 2.x-Reihe von multer enthält die Sicherheits-Fixes, die der 1.x-Reihe fehlen, und ihre package.json setzt die Untergrenze bei Node.js 10.16.0 – nicht bei Node 18. Die Mindestanforderung Node 18 kommt erst mit der 3.x-Reihe, die sich noch im Alpha-Stadium befindet. Der neueste stabile Release im multer-Changelog ist 2.2.0; prüfen Sie das dort, bevor Sie eine Version festschreiben.
Die Bibliotheksoption: FilePond und Konsorten
Wenn Sie zusätzlich Drag & Drop, Upload-Fortschritt, Validierung und mehrere skalierte Varianten benötigen, greifen Sie besser zu FilePond mit seinen Bild-Plugins, statt alles selbst zu bauen. Das Plugin filepond-plugin-image-preview rendert die Vorschau, filepond-plugin-image-resize schreibt die Resize-Metadaten und filepond-plugin-image-transform führt die eigentliche Skalierung durch und liefert Ihnen die Ausgabe-Blobs.
FilePond.registerPlugin(
FilePondPluginImagePreview,
FilePondPluginImageResize,
FilePondPluginImageTransform
);
FilePond.create(document.querySelector('input[type="file"]'), {
imageResizeTargetWidth: 256,
imageResizeMode: 'contain',
});
Legen Sie FilePond auf die Version-4-Reihe fest (@^4), wie es die FilePond-Installationsdokumentation empfiehlt. Die 4.x-Reihe ist die stabile Linie und steht laut Changelog bei 4.32.12, während v5 sich noch in der Beta befindet. Wird unpkg.com/filepond ohne Versionsangabe geladen, folgt das dem latest-Tag: Heute liefert das den aktuellen stabilen Release aus, doch sobald die nächste Major-Version freigegeben wird, landen Sie automatisch darauf.
Speziell für qualitativ hochwertigeres Herunterskalieren wendet Pica (10.0.2) einen echten Resampling-Filter an und kann in einem Web Worker laufen; den Algorithmus wählen Sie über die Option filter, z. B. { filter: 'lanczos3' }. Das Paket browser-image-compression (2.0.2) ist eine weitere Option, allerdings stammt sein letzter Release vom März 2023 und Snyk stuft die Wartung als inaktiv ein – wägen Sie das ab, bevor Sie es einsetzen.
Fallstricke und Best Practices
Dies sind die Fehlerbilder, die Session Replays von Upload-UIs häufig zutage fördern (schleichendes Speicherwachstum, gedrehte Thumbnails und Tabs, die bei riesigen Dateien hängen bleiben):
- Object-URLs freigeben. Rufen Sie stets
URL.revokeObjectURL()auf, wenn Sie eine Vorschau entfernen: JedescreateObjectURL()hält die zugrunde liegende Datei im Speicher, bis die URL explizit freigegeben wird oder das Dokument entladen wird. Unbegrenzt angesammelte Object-URLs sind eine klassische Ursache für allmählich wachsenden Tab-Speicherverbrauch. - FileList ist schreibgeschützt. Da eine
FileListschreibgeschützt ist, können Sie keine Datei aus einem<input>herausschneiden. Führen Sie stattdessen ein eigenes, veränderbares Array und bauen Sie den Upload daraus neu auf. - Vor der Verarbeitung validieren. Kombinieren Sie
accept="image/*"mit einer Laufzeitprüfungfile.type.startsWith('image/')und einer Größenbegrenzung.acceptist ein UX-Hinweis, keine Durchsetzung. - Abmessungen begrenzen, um den Tab zu schützen. Sehr große Bilder können beim Dekodieren den Speicher erschöpfen und den Tab zum Absturz bringen; weisen Sie Dateien oberhalb eines Byte-Schwellwerts zurück und begrenzen Sie
maxSizevor dem Zeichnen. - EXIF-Rotation. Beim Skalieren über ein Canvas kann das EXIF-Orientierungsflag verloren gehen, sodass ein im Hochformat aufgenommenes Handyfoto seitwärts gedreht herauskommt. Testen Sie immer mit echten Hochformat-Fotos von einem Smartphone.
- Qualität beim Herunterskalieren. Bei starken Verkleinerungen kann ein einzelner
drawImage()-Aufruf grob aussehen; skalieren Sie schrittweise oder verwenden Sie eine Bibliothek wie Pica, die einen echten Resampling-Filter für schärfere Thumbnails anwendet. - Canvas-Tainting betrifft ausschließlich Cross-Origin-Bilder, die von anderen Domains geladen werden; vom Benutzer ausgewählte Dateien „verunreinigen” das Canvas nie, eine
crossOrigin-Behandlung ist hier also nicht erforderlich. - Barrierefreiheit. Versehen Sie jedes Vorschau-
<img>mit aussagekräftigemalt-Text und beschriften Sie Entfernen-Buttons mit ARIA, damit die Oberfläche auch ohne visuelle Wahrnehmung des Thumbnails nutzbar ist.
Fazit
Wählen Sie den Weg, der zu Ihrem Ziel passt: eine FileReader- oder Object-URL-Vorschau, wenn Sie lediglich eine visuelle Bestätigung benötigen, und ein Canvas-toBlob-Thumbnail, wenn tatsächlich weniger Bytes über die Leitung gehen sollen. Beginnen Sie mit der oben gezeigten, in ein Promise gekapselten makeThumbnail-Hilfsfunktion, laden Sie Original und Thumbnail gemeinsam in einem FormData hoch und implementieren Sie das revokeObjectURL-Cleanup vom ersten Commit an, statt es erst nachzurüsten, wenn ein Speicherleck sichtbar wird.
FAQs
Reduziert das Erstellen einer Vorschau die Größe der Datei, die der Benutzer hochlädt?
Nein. Eine FileReader-Vorschau mit readAsDataURL oder eine URL.createObjectURL-Vorschau dient rein der Anzeige und lädt exakt die ursprünglichen Bytes hoch – ohne jede Reduktion der übertragenen Datenmenge. Um die hochgeladene Datei tatsächlich zu verkleinern, müssen Sie das Bild in kleineren Abmessungen auf ein Canvas neu zeichnen und es mit canvas.toBlob neu kodieren, um anschließend diesen Blob anstelle des Originals oder zusätzlich dazu hochzuladen.
Warum ist mein per Canvas erzeugtes Thumbnail falsch herum gedreht?
Beim Skalieren eines Bildes über ein Canvas kann das EXIF-Orientierungsflag verloren gehen, das Smartphones bei Hochformat-Fotos speichern – ein korrekt ausgerichtetes Original erscheint dann im Thumbnail seitwärts gedreht. Browser richten ein einfaches img-Element automatisch aus, doch drawImage überträgt diese Orientierung nicht immer auf das Canvas. Testen Sie Ihren Skalierungspfad daher stets mit echten, am Smartphone aufgenommenen Hochformat-Fotos und nicht nur mit Testbildern im Querformat.
Wie kann ich canvas.toBlob mit await verwenden, wenn es nur einen Callback entgegennimmt?
Kapseln Sie es in ein Promise, denn canvas.toBlob ist asynchron und übergibt den Blob an seinen Callback, statt ihn zurückzugeben. Erstellen Sie eine Hilfsfunktion, die new Promise zurückgibt und canvas.toBlob mit resolve aufruft, wobei sie ablehnt, wenn der Callback null erhält. Ein reguläres Canvas bietet keine native Promise-basierte Variante; nur OffscreenCanvas.convertToBlob gibt direkt ein Promise zurück.
Warum kommt mein Thumbnail-Feld nie im multer-Backend an?
Die an multers upload.fields übergebenen Feldnamen müssen exakt mit den FormData.append-Schlüsseln auf dem Client übereinstimmen, andernfalls wird das Feld stillschweigend und ohne Fehlermeldung verworfen. Wenn Sie auf dem Client 'thumbnails' anhängen, muss Ihr Server in upload.fields den Namen 'thumbnails' deklarieren. Prüfen Sie außerdem Ihre multer-Version: Die 2.x-Reihe enthält Sicherheits-Fixes, die der 1.x-Reihe fehlen.