Back

Wie man eine herunterladbare Datei im Browser erstellt

Wie man eine herunterladbare Datei im Browser erstellt

Das Erstellen einer herunterladbaren Datei im Browser kombiniert vier Browser-APIs: ein Blob für die Daten, URL.createObjectURL() für eine In-Memory-Referenz, einen Anker mit dem download-Attribut zum Auslösen des Speichervorgangs und URL.revokeObjectURL() zum anschließenden Freigeben der Referenz. Das vollständige Muster umfasst weniger als zehn Zeilen:

function downloadBlob(data, filename, type) {
  const blob = new Blob([data], { type });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

downloadBlob(JSON.stringify({ ok: true }), 'config.json', 'application/json');

Dies ist das kanonische Muster und deckt den häufigen Anwendungsfall ab, eine generierte CSV-Datei, eine JSON-Konfiguration oder ein Bild zu speichern. Der Standardfall verbirgt jedoch reale Fehlermodi: das download-Attribut, das sich bei Cross-Origin-URLs stillschweigend deaktiviert, iOS Safari, das Downloads in einem neuen Tab öffnet, Object-URLs, die über Component-Re-Renders hinweg Speicher belegen, und Excel, das UTF-8-CSV-Dateien falsch darstellt. Dieser Artikel behandelt das funktionierende Muster, die moderne File System Access API, Streaming für Dateien, die zu groß für den Arbeitsspeicher sind, sowie plattformspezifische Eigenheiten, die Downloads in der Produktion zum Scheitern bringen.

Wichtigste Erkenntnisse

  • Der kanonische clientseitige Download lautet: new Blob([data], { type })URL.createObjectURL() → ein Anker mit dem download-Attribut → click()URL.revokeObjectURL().
  • Die Lebensdauer einer Object-URL ist an das Dokument gebunden, das sie erstellt hat. Daher sollte sie in einer Framework-Cleanup-Funktion widerrufen werden (useEffect-Rückgabe, Vue onUnmounted), anstatt sich auf eine nicht verifizierte zeitliche Abhängigkeit zu a.click() zu verlassen.
  • showSaveFilePicker() ist die einzige browsernative Möglichkeit, Benutzern die Wahl eines Speicherorts zu ermöglichen, ist jedoch experimentell und nur in Chromium verfügbar. Daher ist eine Feature-Erkennung mit Fallback auf das Anker-Muster zwingend erforderlich.
  • Excel öffnet eine UTF-8-CSV-Datei nur dann korrekt, wenn die Datei eine BOM enthält. Daher muss \uFEFF dem String vorangestellt werden.
  • iOS Safari hatte historisch gesehen eine unzuverlässige Unterstützung für Downloads mit dem download-Attribut, weshalb Downloads dort häufig in einem neuen Tab geöffnet werden, anstatt gespeichert zu werden.

Das kanonische Muster: Eine Datei mit JavaScript im Browser herunterladen

Die zuverlässige Methode zum Herunterladen einer mit JavaScript generierten Datei besteht darin, einen Blob zu erstellen, eine Object-URL mit URL.createObjectURL() zu erzeugen, diese dem href eines Ankers zuzuweisen, das download-Attribut auf den Dateinamen zu setzen, den Anker programmatisch anzuklicken und anschließend die URL mit URL.revokeObjectURL() freizugeben. Der Blob-Konstruktor ermöglicht es, den MIME-Typ unabhängig von den Daten festzulegen, und die Object-URL ist eine kurze Referenz (blob:https://…), zu der der Anker navigieren kann.

Vermeiden Sie Data-URIs als Standard. Data-URIs sind die falsche Standardwahl für die clientseitige Dateigenerierung: Base64 kodiert je 3 Bytes als 4 Zeichen, was die Nutzlastgröße vor dem Padding um etwa ein Drittel vergrößert (gemäß RFC 4648), und der gesamte kodierte String muss als DOM-Attributwert im Arbeitsspeicher vorgehalten werden. Die aktuellen Größenbeschränkungen für data:-URLs liegen bei 512 MB in Chromium und Firefox sowie 2048 MB in Safari/WebKit (MDN data: URL-Referenz) — doch der Kodierungsaufwand und die Kosten des In-Memory-Strings machen Blob weit vor diesen Obergrenzen zur besseren Standardwahl.

Der Anker muss für click() in aktuellen Browsern nicht an das DOM angehängt sein, was den Helfer in sich geschlossen hält:

// Funktioniert in Chrome 14+, Firefox 20+ (download-Attribut, HTML5).
// caniuse: https://caniuse.com/download
function downloadBlob(data, filename, type = 'application/octet-stream') {
  const blob = new Blob([data], { type });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a); // für maximale Kompatibilität angehängt
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}

const csv = 'name,role\nAda,Engineer\nGrace,Architect';
downloadBlob(csv, 'team.csv', 'text/csv;charset=utf-8;');

Der Aufruf URL.revokeObjectURL(url) ist wichtiger, als die meisten Beispiele vermuten lassen. Die Lebensdauer einer Object-URL ist an das Dokument gebunden, das sie erstellt hat, sodass sie im Arbeitsspeicher verbleibt, bis sie widerrufen wird oder das Dokument entladen wird. In einem einmalig ausgeführten Skript ist das unproblematisch; in einer komponentenbasierten Anwendung, in der der Helfer bei jedem Button-Klick ausgeführt wird, akkumulieren sich nicht freigegebene URLs. Das synchrone Widerrufen oben ist hier sicher, da in dieser Funktion nichts über den Aufruf hinaus bestehen bleibt — aber wie der Framework-Abschnitt zeigt, ist diese Platzierung innerhalb einer Komponente falsch.

Das HTML-eigene download-Attribut und sein stilles Cross-Origin-Versagen

Für eine Datei, die bereits auf Ihrer eigenen Origin gehostet wird, benötigen Sie überhaupt kein JavaScript — fügen Sie einfach das download-Attribut zu einem gewöhnlichen Anker hinzu:

<a href="/reports/q3.pdf" download="q3-report.pdf">Bericht herunterladen</a>

Das download-Attribut weist den Browser an, die verlinkte Ressource zu speichern, anstatt zu ihr zu navigieren. Es wurde in HTML5 eingeführt und wird in Chrome 14+ und Firefox 20+ unterstützt (caniuse: download-Attribut). Es akzeptiert auch blob:- und data:-URLs, weshalb es gut mit dem oben beschriebenen kanonischen Muster harmoniert.

Der Fehlermodus tritt bei Cross-Origin-URLs auf. Bei Cross-Origin-Downloads wird das download-Attribut nur dann berücksichtigt, wenn die Antwort auch Content-Disposition: attachment enthält; ohne diesen Header ignoriert der Browser das Attribut, und der Link ist kein zuverlässiger erzwungener Download (gemäß dem WHATWG HTML Standard, downloading resources). Dies ist eine häufige Fehlerquelle: Dasselbe Markup, das eine Same-Origin-Datei herunterlädt, navigiert zu einer Datei, die von einem CDN auf einer anderen Origin bereitgestellt wird, oder rendert sie inline. Wenn Sie den Server kontrollieren, setzen Sie den Header dort. Wenn nicht, rufen Sie die Datei ab und stellen Sie sie als Blob von Ihrer eigenen Origin bereit:

// Eine Cross-Origin-Datei als Same-Origin-Blob bereitstellen, damit `download` berücksichtigt wird.
async function downloadCrossOrigin(remoteUrl, filename) {
  const res = await fetch(remoteUrl); // erfordert CORS, um den Abruf zu erlauben
  const blob = await res.blob();
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

File System Access API: Den Benutzer einen Speicherort auswählen lassen

Die showSaveFilePicker()-Methode der File System Access API ist die einzige browsernative Möglichkeit, dem Benutzer die Wahl eines Speicherorts und Dateinamens vor dem Schreiben zu ermöglichen. Sie öffnet den betriebssystemeigenen Speicherdialog, gibt ein File-Handle zurück und ermöglicht das Schreiben über einen FileSystemWritableFileStream. MDN kennzeichnet sie als „eingeschränkt verfügbar” und „experimentell”; sie wurde in Chromium-basierten Browsern ab Version 86 eingeführt (Chrome Status) und wird weder in Firefox noch in Safari unterstützt, was eine Feature-Erkennung mit Fallback auf das Anker-Muster für den Produktionseinsatz unerlässlich macht.

showSaveFilePicker() wirft eine DOMException mit dem Namen AbortError, wenn der Benutzer den Dialog schließt. Behandeln Sie diesen Fall daher explizit, anstatt ihn als Fehler zu werten:

async function saveFile(data, filename, type = 'application/octet-stream') {
  // Feature-Erkennung. Hinweis: Die Eigenschaft kann in manchen
  // Sandbox-iframe-Kontexten existieren, aber beim Aufruf einen Fehler werfen,
  // daher ist der Aufruf selbst weiterhin abgesichert.
  if ('showSaveFilePicker' in window) {
    try {
      const handle = await window.showSaveFilePicker({
        suggestedName: filename,
        types: [{ description: 'File', accept: { [type]: ['.' + filename.split('.').pop()] } }],
      });
      const writable = await handle.createWritable();
      await writable.write(new Blob([data], { type }));
      await writable.close();
      return;
    } catch (err) {
      if (err.name === 'AbortError') return; // Benutzer hat abgebrochen, kein Fehler
      // Bei jedem anderen Fehler zum Anker-Fallback übergehen
    }
  }
  // Fallback: Anker + Object-URL (keine Standortwahl; Browserstandard).
  downloadBlob(data, filename, type);
}

Die Prüfung 'showSaveFilePicker' in window ist die richtige Absicherung, aber nicht wasserdicht: In manchen Sandbox-iframe-Kontexten existiert die Eigenschaft, wirft jedoch beim Aufruf einen Fehler. Deshalb umschließt try/catch den eigentlichen Aufruf und nicht nur die Erkennung.

Streaming großer Dateien, die den verfügbaren Arbeitsspeicher überschreiten

Streaming umgeht die Arbeitsspeichergrenze von Blob, indem Chunks direkt auf die Festplatte geschrieben werden, anstatt die gesamte Nutzlast im RAM zu puffern. Es gibt keinen festen Byte-Schwellenwert für diese Grenze — er hängt vom Gerät, dem Betriebssystem und dem verfügbaren Heap des Browsers ab. Zwei Streaming-Ansätze vermeiden das vollständige Puffern der Datei.

In Chromium kann inkrementell über den FileSystemWritableFileStream geschrieben werden, der von showSaveFilePicker() zurückgegeben wird. Jeder write()-Aufruf fügt einen Chunk zum beschreibbaren Stream hinzu, sodass die Anwendung die gesamte Datei nicht auf einmal im Arbeitsspeicher halten muss:

// Nur Chromium 86+. Schreibt in Chunks; hält nie die vollständige Datei im Arbeitsspeicher.
async function streamToDisk(filename, chunks) {
  const handle = await window.showSaveFilePicker({ suggestedName: filename });
  const writable = await handle.createWritable();
  for await (const chunk of chunks) {
    await writable.write(chunk); // chunk: string | ArrayBuffer | Blob
  }
  await writable.close();
}

Für Browser ohne File System Access API ist StreamSaver.js der browserübergreifende Ansatz. Laut seinem README speichert StreamSaver.js große clientseitig generierte Dateien, indem es einen beschreibbaren Stream erstellt und einen servergesteuerten Download mithilfe von Response-Headern sowie einem Service Worker emuliert, sodass die Daten beim Streamen auf die Festplatte geschrieben werden, anstatt in einem einzigen Blob zusammengestellt zu werden. Es basiert auf dem WHATWG Streams Standard:

import streamSaver from 'streamsaver';

// Emuliert einen Server-Download über einen Service Worker; Daten werden gestreamt auf die Festplatte geschrieben.
function streamWithStreamSaver(filename, readableStream) {
  const fileStream = streamSaver.createWriteStream(filename);
  // readableStream: ein WHATWG ReadableStream aus Uint8Array-Chunks
  return readableStream.pipeTo(fileStream);
}

Setzen Sie Streaming ein, wenn die Ausgabe wirklich groß ist — Exporte im mehrere hundert Megabyte-Bereich, generierte Videos oder ein lang laufender Datenfeed. Für eine CSV-Datei mit einigen tausend Zeilen ist das kanonische Blob-Muster einfacher und ausreichend.

iOS-Safari-Eigenheiten und Lösungsansätze

iOS Safari hatte historisch gesehen eine unzuverlässige Unterstützung für Dateien, die über das download-Attribut generiert und heruntergeladen werden (WebKit-Bug 167341), sodass eine generierte Datei häufig in einem neuen Tab geöffnet oder inline gerendert wird, anstatt gespeichert zu werden — iOS ist damit die Plattform, auf der Download-Code in der Produktion am häufigsten versagt. Das FileSaver.js-README dokumentiert die praktische Konsequenz: Auf iOS muss saveAs() innerhalb einer Benutzerinteraktion wie onClick ausgeführt werden, und setTimeout verhindert dessen Auslösung; aufgrund von iOS-Einschränkungen kann saveAs() ein neues Fenster öffnen, anstatt einen Download auszulösen (FileSaver.js README).

Die konkreten Lösungsansätze:

  • Den Download synchron mit der Benutzergeste halten. Den Klick direkt im Event-Handler auslösen, nicht nach einem await oder setTimeout, das ihn von der Geste trennt. Wenn asynchrone Vorarbeit notwendig ist, die Daten vor dem Klick des Benutzers abrufen und vorbereiten, dann den Speichervorgang synchron auslösen.
  • Dem Benutzer einen expliziten Fallback anbieten. Wenn ein neuer Tab mit der inline gerenderten Datei geöffnet wird, einen Hinweis wie „Tippen Sie auf Teilen → In Dateien sichern” anzeigen, da der Benutzer den Speichervorgang manuell abschließen muss.
  • Für Textexporte auf iOS text/plain oder einen anzeigbaren Typ bevorzugen und akzeptieren, dass die Datei möglicherweise in einem Viewer geöffnet wird, den der Benutzer dann speichert, anstatt einen erzwungenen Download anzustreben, den die Plattform nicht zuverlässig unterstützt.

Diese Fehler sind für Server-Logs und Analytics unsichtbar, da keine Anfrage das Backend erreicht und keine Ausnahme ausgelöst wird. Session-Replays von Download-Flows in der Produktion zeigen häufig, dass Benutzer den Auslöser mehrfach anklicken, weil kein sichtbares Feedback ausgelöst wurde — auf iOS erscheint dies oft als ein neuer Tab, der sich öffnet und den der Benutzer sofort schließt, bevor er verwirrt zurückkehrt. Session-Replay ist eine der wenigen Techniken, die diese Klasse von stillen, gesten- und plattformgebundenen Fehlern sichtbar macht.

// iOS-bewusst: Zuerst asynchrone Arbeit erledigen, dann synchron innerhalb der Geste speichern.
async function prepareThenDownload(button, getData, filename, type) {
  const data = await getData(); // Netzwerk-/CPU-Arbeit findet vor dem Klick-Pfad statt
  button.onclick = () => {
    // synchron mit der Benutzergeste — kein setTimeout, kein await hier
    downloadBlob(data, filename, type);
  };
}

MIME-Typ-Fallstricke: Inline-Anzeige, Excel-CSV und Zeilenenden

Der MIME-Typ, den Sie dem Blob-Konstruktor übergeben, hilft dem Browser bei der Interpretation der Datei. Ein falscher Typ ist eine häufige Ursache dafür, dass heruntergeladene Inhalte unerwartet geöffnet oder falsch verarbeitet werden. Setzen Sie einen Typ, der zu den Daten passt, und kombinieren Sie ihn mit dem download-Attribut, damit der Browser das Ergebnis als Anhang behandelt.

Der am häufigsten gemeldete CSV-Fehler ist, dass Sonderzeichen wie Umlaute oder Akzentzeichen beim Öffnen der Datei in Excel als unleserliche Zeichen dargestellt werden. Die Lösung ist eine Byte-Order-Mark. Excel öffnet eine UTF-8-CSV-Datei korrekt, wenn sie mit einer BOM gespeichert wird. Daher muss eine solche dem String vorangestellt werden (Microsoft Support: UTF-8-CSV-Dateien in Excel öffnen):

// Ohne BOM: Excel kann bei Texten mit Sonderzeichen (é, ü, ñ) fehlerhafte Zeichen anzeigen.
// Mit BOM: Excel öffnet die UTF-8-Datei korrekt.
function downloadCSV(csvString, filename = 'export.csv') {  
  downloadBlob('\uFEFF' + csvString, filename, 'text/csv;charset=utf-8;');  
}

Zwei weitere Details aus RFC 4180, das text/csv als MIME-Typ für CSV registriert: Die Spezifikation definiert CRLF (\r\n) als Datensatztrennzeichen. Verwenden Sie daher \r\n anstelle von \n zwischen Zeilen für maximale Tabellenkalkulationskompatibilität. Felder, die Kommas, Anführungszeichen oder Zeilenumbrüche enthalten, müssen in doppelte Anführungszeichen eingeschlossen werden, wobei interne Anführungszeichen verdoppelt werden.

Vermeiden Sie application/octet-stream als universellen Erzwingungsschalter für Downloads. Es ist der von IANA registrierte Typ für beliebige Binärdaten, aber kein zuverlässiger Mechanismus zum Erzwingen von Downloads — das FileSaver.js-README weist darauf hin, dass die Verwendung von application/octet-stream zum Erzwingen von Downloads Probleme in Safari verursachen kann. Verwenden Sie den korrekten, spezifischen Typ und verlassen Sie sich auf das download-Attribut (oder Content-Disposition), um das Speichern zu erzwingen.

Framework-Fallstricke: SSR, Object-URL-Lebenszyklus und Speicherlecks

Zwei Komponentenlebenszyklus-Fehler brechen das kanonische Download-Muster in React, Vue und Svelte: der Aufruf von DOM-APIs während des serverseitigen Renderings (wo document undefiniert ist) und das Widerrufen von Object-URLs zum falschen Zeitpunkt im Komponentenlebenszyklus. Beide haben dieselbe Grundursache — Komponenten werden auf dem Server gerendert und auf dem Client neu gerendert, aber das einfache Muster setzt ein einzelnes Dokument und eine einzige Ausführung voraus.

Gegen SSR absichern. In Next.js, Nuxt oder SvelteKit kann Komponentencode dort ausgeführt werden, wo document undefiniert ist; der Aufruf von document.createElement wirft dort einen Fehler. Sichern Sie jeden Download-Helfer hinter einer Laufzeitprüfung ab:

function triggerDownload(data, filename, type) {
  if (typeof document === 'undefined') return; // SSR-Absicherung
  downloadBlob(data, filename, type);
}

Object-URLs im Cleanup widerrufen, nicht sofort. Das synchrone URL.revokeObjectURL(url) aus dem kanonischen Muster ist für einen einmaligen Helfer geeignet, der abgeschlossen wird, bevor etwas anderes ausgeführt wird. Wenn Sie jedoch eine Blob-URL im State speichern, um sie als href oder src zu verwenden, bricht ein zu frühes Widerrufen den Download ab, und ein niemals erfolgtes Widerrufen führt zu Speicherlecks über Re-Renders hinweg. Die bewährte Regel: Die Lebensdauer einer Object-URL ist an das Dokument gebunden, das sie erstellt hat. Widerrufen Sie sie, sobald sie nicht mehr benötigt wird. In komponentenbasierten Frameworks sollten Sie die Blob-URL im State speichern und in einer Cleanup-Funktion widerrufen, anstatt sich auf eine nicht verifizierte zeitliche Abhängigkeit zu a.click() zu verlassen.

import { useEffect, useState } from 'react';

function DownloadLink({ data, filename, type = 'application/json' }) {
  const [url, setUrl] = useState(null);

  useEffect(() => {
    const blob = new Blob([data], { type });
    const objectUrl = URL.createObjectURL(blob);
    setUrl(objectUrl);
    // Cleanup wird beim Unmount und vor dem erneuten Ausführen des Effects aufgerufen:
    return () => URL.revokeObjectURL(objectUrl);
  }, [data, type]);

  // ANTI-PATTERN — NICHT hier widerrufen:
  //   <a href={url} download={filename} onClick={() => URL.revokeObjectURL(url)}>
  // Das Widerrufen innerhalb des Klick-Handlers kann die URL freigeben, bevor der Download startet.

  return url ? <a href={url} download={filename}>Herunterladen</a> : null;
}

Das Vue-Äquivalent erstellt die URL in onMounted (oder einem watch) und widerruft sie in onUnmounted; Svelte verwendet onDestroy. In allen drei Frameworks ist das Speicherleck, das sich einschleicht, eine neue Object-URL, die bei jedem Re-Render oder jedem Button-Klick erstellt wird, ohne ein entsprechendes Widerrufen — der Speicherverbrauch wächst für die gesamte Lebensdauer des Dokuments.

Für ältere Browser, denen das download-Attribut vollständig fehlt, bleibt FileSaver.js ein einzeiliges Polyfill — auf modernen Zielen ersetzt das oben beschriebene Plattformmuster es jedoch.

Fazit

Die Sequenz Blob → Object-URL → Anker → download → Widerrufen deckt den Großteil der clientseitigen Download-Aufgaben ab, mit showSaveFilePicker() als Chromium-exklusivem Upgrade, wenn Benutzer einen Speicherort wählen müssen, und Streaming als Ausweg, wenn Dateien den Arbeitsspeicher überschreiten. Die Fehler, die in der Produktion auftreten, liegen selten im Standardfall — es sind das Cross-Origin-Attribut, das stillschweigend navigiert, die CSV-Datei, die eine BOM benötigt, der iOS-Tab, der sich öffnet, anstatt zu speichern, und die Object-URL, die zum falschen Zeitpunkt widerrufen wird. Verankern Sie das Cleanup im Komponentenlebenszyklus, erkennen Sie experimentelle APIs per Feature-Detection, bevor Sie sie aufrufen, und testen Sie den Flow auf einem echten iOS-Gerät, bevor Sie ihn ausliefern.

FAQs

Ein Blob speichert Rohdaten und wird durch eine kurze Object-URL referenziert, die mit URL.createObjectURL() erstellt wird, während eine Data-URI die gesamte Nutzlast als Base64-String direkt in der URL einbettet. Base64-Kodierung vergrößert die Nutzlast vor dem Padding um etwa ein Drittel, und der vollständige kodierte String muss als Attributwert im Arbeitsspeicher vorgehalten werden. Blob ermöglicht es, einen MIME-Typ unabhängig von den Daten festzulegen, und vermeidet beide Kosten, was ihn zur besseren Standardwahl für die clientseitige Dateigenerierung macht.

Das download-Attribut wird für Same-Origin-URLs berücksichtigt, aber für Cross-Origin-URLs ignoriert, es sei denn, die Antwort enthält auch einen Content-Disposition: attachment-Header, gemäß dem WHATWG HTML Standard. Eine Datei, die von einem CDN auf einer anderen Origin bereitgestellt wird, wird inline navigiert oder gerendert, anstatt heruntergeladen zu werden. Die Lösung besteht darin, den Header auf dem Server zu setzen, den Sie kontrollieren, oder die Cross-Origin-Datei abzurufen und als Same-Origin-Blob über URL.createObjectURL() bereitzustellen.

Nein. Die showSaveFilePicker()-Methode der File System Access API wird nur in Chromium-basierten Browsern ab Version 86 unterstützt und ist weder in Firefox noch in Safari implementiert. MDN kennzeichnet sie als 'eingeschränkt verfügbar' und 'experimentell'. Aus diesem Grund muss Produktionscode eine Feature-Erkennung mit 'showSaveFilePicker' in window durchführen und auf das Anker- und Object-URL-Muster zurückfallen. Umschließen Sie den Aufruf selbst mit try/catch, da die Eigenschaft in manchen Sandbox-iframe-Kontexten existieren, aber einen Fehler werfen kann, und sie wirft AbortError, wenn der Benutzer den Dialog schließt.

Das Widerrufen der Object-URL, bevor der Download initiiert wird, bricht ihn ab, da der Browser die Blob-Referenz, auf die der Anker zeigt, nicht mehr auflösen kann. In einem einmaligen Helfer, der synchron abgeschlossen wird, ist das Widerrufen direkt nach click() sicher. In einer Komponente, die die Blob-URL im State für ein href oder src speichert, sollte sie stattdessen in einer Cleanup-Funktion widerrufen werden, beispielsweise im Rückgabewert von useEffect, in Vues onUnmounted oder in Sveltes onDestroy. Ein niemals erfolgtes Widerrufen führt hingegen zu Speicherlecks über Re-Renders hinweg für die gesamte Lebensdauer des Dokuments.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay