Startseite / Artikel / Fünfundzwanzig JavaScript-Interview-Szenarien aus Produktionsfehlern

Fünfundzwanzig JavaScript-Interview-Szenarien aus Produktionsfehlern

Rassen, Thrash, idempotente Zahlungen, Lecks, Hydratation, WeakMap-Caches, instabile Tests sowie gemeinsame Fetch-Schichten – mit Antworten, die Interviewer tatsächlich wollen.

4513 Wörter

Von KI

Fünfundzwanzig JavaScript-Fragen im Produktionsstil

In Interviews werden immer noch Fragen gestellt wie, was typeof null zurückgibt, wie Hoisting funktioniert oder wie man bind polyfillt. Solche Fragen sind leicht zu bewerten – und nach einem Wochenende des Auswendiglernens auch leicht zu fälschen. Dann liefert derselbe Kandidat eine Suchleiste, die Ergebnisse für eine Abfrage anzeigt, die der Benutzer bereits gelöscht hat.

Teams mit echtem Traffic haben das Format geändert. Anstatt um einen Vortrag über den Event Loop zu bitten, zeigen sie nach einer Änderung des Zeitraums einen eingefrorenen Analysebildschirm an, übergeben den Codeausschnitt und fragen, was schiefgeht. Dasselbe Grundwissen – unterschiedliche Prüfungsform. Die eine Methode prüft das Auswendiglernen, die andere, ob jemand in Gegenwart eines Beobachters über ein unbekanntes System nachdenken kann.

Die Probleme treten am schnellsten in drei Bereichen auf: asynchrones Verhalten unter echten Netzwerken (antworten außerhalb der Reihenfolge, Promises, die abgearbeitet werden, aber keine Bedeutung mehr haben), Speicher (auf einem Laptop, der sechs Minuten lang offen bleibt, stürzt nichts ab) sowie Fehler (ein Anbieter sendet HTTP 200 mit einem HTML-Fehlerinhalt, wodurch response.json() um 2 Uhr morgens abstürzt).

Danach folgen fünfundzwanzig Szenarien, die aus Fehlern stammen, die tatsächlich in der Produktion auftreten: langsame Dashboards, doppelte Abfragen, zunehmender Speicherverbrauch bei Route-Änderungen, veraltete Suchergebnisse, doppelte Abrechnungen, fehlerhafte Authentifizierungsannahmen, Tabellen mit zwanzigtausend Zeilen sowie APIs, die zu 96 Prozent der Zeit „verfügbar“ sind. Jeder Punkt umfasst die Situation, die Antwort, nach der die Interviewer suchen, einen kurzen Codeausschnitt, einen häufigen falschen Ansatz, eine mögliche Folgehandlung sowie das, was gemessen wird.

Beispiele werden zunächst im JavaScript gezeigt, wobei nur dort TypeScript-Hinweise angegeben werden, wo die Typen die Antwort verändern. Ebenen: Anfänger für eine Grundvorbereitung, Mittelstufe für die meisten fortgeschrittenen Anwendungen, Fortgeschrittene für Gespräche mit Mitarbeitern und Führungskräften.

Anfänger – Grundlagen der Produktion

Dies unterscheidet diejenigen, die bereits fertige Produkte bereitgestellt haben, von denen, die nur Tutorials abgeschlossen haben. Nichts Kompliziertes – all das führt in echten Anwendungen zu Problemen.

Debounce vs. Throttle für Suchfunktionen und Scrollen

Szenario. Ein Suchfeld sendet bei jeder Tastenbetätigung eine Anfrage. Das Produkt verlangt weniger Aufrufe, ohne dass das Tippen langsam wirkt. Scroll-Handler benötigen hingegen kontinuierliche, aber begrenzte Aktualisierungen.

Frage. Wann ist debounce das richtige Werkzeug im Vergleich zu Throttle – und wie implementiert man es sicher in React?

Antwort. Das Throttling garantiert Aufrufe in einem festen Rhythmus – ideal für Scrollen und Anpassung der Größe. Debouncing wartet, bis das Tippen pausiert, was der Suchabsicht entspricht. Beginnen Sie mit einem Zeitraum von etwa ein Viertel Sekunde bis dreihundert Millisekunden und passen Sie dies anhand von Metriken an. Kombinieren Sie Debouncing mit einer Mindestanfragenlänge sowie der Möglichkeit zur Anfragestornierung; alleines Debouncing behebt keine aus dem Ruder gelaufenen Antworten.

function debounce(fn, wait = 250) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

const search = debounce((q) => {
  if (q.length < 2) return;
  fetchResults(q);
});

Falscher Ansatz. Jedes Mal bei der Neuzeichnung einen neuen, debounceden Wrapper erstellen. Die Timer werden mit neuen Schließfunktionen zurückgesetzt, wodurch Debouncing nie wirklich wirkt. Stabilisieren Sie die Situation mit useMemo/useRef und löschen Sie die Funktion beim Entfernen des Elements.

Nachfragen. Der Benutzer tippt schnell und leert anschließend das Feld. Wird trotzdem ein debounceder Aufruf mit dem letzten nicht leeren Wert ausgelöst? Wie wirken sich die Tornierung beim Entfernen des Elements und beim Leeren des Feldes gegenseitig aus?

Messungsergebnis. Die Auswahl des Tools wird durch die UX-Ziele sowie durch die Kompatibilität mit Schließmechanismen bestimmt, die bei verschiedenen Darstellungen erhalten bleiben.

Zwanzigtausend Klick-Listener

Szenario. Ein Datengitter verknüpft für jede Zeilenaktion einen onClick. Bei 20.000 Zeilen benötigt die Seite Sekunden, bis sie interaktiv wird, und der Speicherverbrauch steigt beim Blättern.

Frage. Wie sollte die Ereignisverarbeitung umstrukturiert werden?

Antwort. Verwenden Sie Ereignisdelegation. Ein einziger Listener im Container liest das Ziel ab, während die Ereignisse nach oben weitergeleitet werden – eine einzige Funktion im Speicher, keine erneute Verknüpfung bei Änderungen der Zeilen, funktioniert auch für später hinzugefügte Zeilen. Identifizieren Sie Zeile und Aktion mithilfe von Datenattributen sowie closest, da Klicks oft auf einem Icon innerhalb der Schaltfläche und nicht direkt auf der Schaltfläche selbst landen.

grid.addEventListener('click', (event) => {
  const button = event.target.closest('[data-action]');
  if (!button || !grid.contains(button)) return;

  const { action } = button.dataset;
  const rowId = button.closest('tr')?.dataset.rowId;
  handleAction(action, rowId);
});

Falscher Ansatz. Immer noch werden Tausende von Zeilen-Listenern konfiguriert in der Hoffnung, dass eine Aufräumschleife sie entfernt. Die Einrichtungskosten bleiben bestehen, und nicht übereinstimmende Funktionseinträge scheitern stillschweigend daran, entfernt zu werden.

Nachfolgefragen. In React 17+ – wo registriert die Bibliothek ihren Wurzel-Listener, und wie sollten delegierte native Handler mit dem synthetischen Ereignisbubbling koexistieren?

Bewertungskriterium. Ob der Kandidat die Anzahl der Listener als echtes Leistungsbudget betrachtet und nicht nur als stilistische Entscheidung.

Eine Promise, die bereits abgearbeitet wurde, ist nicht dasselbe wie eine Promise, die weiterhin relevant ist.

Mittelstufe – Asynchronität, Speicher und Leistung

Hier werden die meisten fortgeschrittenen Vorstellungsgespräche entschieden. Die Fragen ähneln Debugging-Sitzungen, denn genau das ist die Aufgabe.

Das Dashboard, das zwei Sekunden lang einfriert

Szenario. Wenn der Zeitraum geändert wird, friert das Tab-Fenster ein. Klicks haben keine Wirkung, Animationen stoppen, und der Spinner dreht sich nicht. Der eigentliche Netzwerkaufruf dauert 180 ms.

Frage. Warum animiert sich der Spinner nicht, und wohin ist die Zeit verschwunden?

Antwort. Der Hauptthread verarbeitet gleichzeitig Skripte, Layouts, Zeichnungen und Eingaben. Wenn der Aufrufstapel lange genug blockiert ist, kann selbst ein Spinner nicht gezeichnet werden, weil der Frame, in dem er angezeigt werden sollte, nie ausgeführt wird. Die 180 ms beziehen sich auf den Netzwerkaufruf; das Einfrieren entsteht durch die anschließende synchrone Verarbeitung – das Parsen eines riesigen JSON-Datensatzes sowie die Anwendung von map/filter auf Zehntausende von Zeilen, wobei für jede Zeile ein neuer Array allokiert wird.

Verlagern Sie aufwändige Transformationen in einen Web Worker, teilen Sie die Arbeit in Blöcke auf und geben Sie zwischen diesen Blöcken wieder Kontrolle ab. Wenn möglich, fordern Sie vom Backend aggregierte Daten an.

// Yield to the event loop between chunks so input and paint can run
async function processInChunks(items, fn, chunkSize = 500) {
  const out = [];
  for (let i = 0; i < items.length; i += chunkSize) {
    for (const item of items.slice(i, i + chunkSize)) out.push(fn(item));
    await new Promise((r) => setTimeout(r, 0));
  }
  return out;
}

Falscher Ansatz. Das Hinzufügen von async/await in einen rechenintensiven Schleifenzyklus und die Bezeichnung als nicht blockierend. Es entsteht kein zusätzlicher Thread; die synchrone Ausführung blockiert weiterhin das Tab-Fenster.

Nachfragen. Erklären Sie den Unterschied zwischen Mikro- und Makrotasks in diesem Blockierverhalten sowie warum das Ausführen von await Promise.resolve() weiterhin lange synchrone Phasen verursacht.

Messungen. Die Konkurrenzfähigkeit von JS als Arbeitsplanung verstehen und erkennen, wann Arbeiterprozesse benötigt werden.

Von KI

Suchergebnisse für eine Abfrage, die der Benutzer bereits gelöscht hat

Szenario. Das Eingeben von „sam“ und anschließende Verfeinerung auf „samantha“ zeigt manchmal noch Ergebnisse für „sam“, nachdem bereits Ergebnisse für „samantha“ angezeigt wurden. Auf schnellen Verbindungen schwer nachzuvollziehen.

Frage. Was geschieht hier, und welche ist die richtige Lösung?

Antwort. Ein Wettlauf zwischen aus-of-Order-Antworten. Zwei Anfragen werden gesendet; die langsamere gehört zur älteren Abfrage; wer zuletzt fertig wird, gewinnt die Aktualisierung des Zustands. Beide Gegenmaßnahmen sollten bevorzugt werden: Die vorherige Anfrage mit AbortController abbrechen und den Zustandsschreibvorgang schützen, sodass nur die Antwort zur aktuellen Eingabe gespeichert werden kann.

const controllerRef = useRef(null);

async function search(query) {
  controllerRef.current?.abort();
  const controller = new AbortController();
  controllerRef.current = controller;

  try {
    const res = await fetch(`/api/customers?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });
    setResults(await res.json());
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
  }
}

Falscher Ansatz. Den Debounce auf eine volle Sekunde ausdehnen und dann als Sieg betrachten. Solche Wettläufe werden seltener, die Benutzererfahrung verschlechtert sich, und langsame Netzwerke ordnen die Antworten weiterhin um.

Nachfrage. Wann würde eine monoton steigende Anfrage-ID den AbortController bei der Ablehnung veralteter Suchdaten übertreffen?

Messbarkeit. Die Wettläufe richtig benennen und Störungen durch Abbrüche aus den Fehlerdashboards ausschließen.

Die gleiche Anfrage, fünfmal

Szenario. Fünf Komponenten rufen jeweils bei Laden /api/current-user auf. Das Netzwerk-Tab zeigt fünf identische Anfragen; gelegentlich ist eine Antwort nach einem Profilupdate veraltet.

Frage. Wie entfernt man doppelte Anfragen im Lauf, ohne die gesamte Datenschicht umschreiben zu müssen?

Antwort. Cachen Sie die Promise, nicht das Ergebnis. Schlüsseln Sie ein Map nach der Identität der Anfrage, geben Sie die gemeinsame Promise an jeden Aufrufer zurück und löschen Sie den Schlüssel, sobald die Promise abgeschlossen ist, damit Fehler erneut versucht werden können und später frische Daten abgerufen werden können. Bibliotheken wie TanStack Query und SWR fügen zu dieser Idee noch Regeln zur Invaliderung und Veraltungsprüfung hinzu.

const inFlight = new Map();

export function dedupedFetch(key, fetcher) {
  if (inFlight.has(key)) return inFlight.get(key);

  const promise = fetcher().finally(() => inFlight.delete(key));
  inFlight.set(key, promise);
  return promise;
}

// All five callers receive the same promise
const user = await dedupedFetch('/api/me', () => fetch('/api/me').then((r) => r.json()));

Falscher Ansatz. Den abgeschlossenen Payload dauerhaft in einer variablen auf Modulebene speichern. Die doppelten GET-Anfragen verschwinden, danach zeigt die Benutzeroberfläche weiterhin den Benutzer von gestern, bis jemand neu lädt.

Nachfolgefrage. Wenn zwei Aufrufer unterschiedliche Abbruchsignale an ein gemeinsames, laufendes Promise hängen, welche Stornierungsrichtlinie sorgt dafür, dass beide korrekt funktionieren?

Lösung. Laufende Promises sorgfältig teilen und die Invalidation im Voraus planen.

Ein unzuverlässiger Anbieter macht die Seite unbrauchbar

Szenario. Profilinformationen, die Rechnungsstellung sowie ein Widget mit Empfehlungen von Drittanbietern werden gleichzeitig geladen. Der Anbieter ist zu ~96 % verfügbar. Wenn er ausfällt, fehlt auf der gesamten Seite die Funktion und die Rechnungsstellung ist nicht sichtbar.

Frage. Wie sollte der Fetch-Aufruf umstrukturiert werden, und inwiefern unterscheiden sich hier Promise.all und Promise.allSettled?

Antwort. Promise.all lehnt ab, sobald eine der Eingaben fehlschlägt, und ignoriert die erfolgreichen Elemente. Promise.allSettled löst sich immer mit dem Status jedes Eintrags auf – zeichnen Sie das Erfolgreiche aus und behandeln Sie den Rest als fehlerhaft. Behalten Sie die Abrechnung und das Profil auf dem kritischen Pfad; umschließen Sie Empfehlungen mit einer kurzen Frist, damit ein fehlerhafter Anbieter die Gesamtprozessabwicklung nicht blockieren kann, selbst wenn er später einen Erfolg meldet.

const [profile, billing, recs] = await Promise.allSettled([
  getProfile(),
  getBilling(),
  withTimeout(getRecommendations(), 2000),
]);

if (profile.status === 'rejected' || billing.status === 'rejected') {
  return renderError();
}
render({
  profile: profile.value,
  billing: billing.value,
  recs: recs.status === 'fulfilled' ? recs.value : [],
});

Falscher Ansatz. Fehlschläge auf null abbilden, damit Promise.all weiterhin funktioniert. Dadurch geht der Grund für die Ablehnung verloren und jeder Fehler erscheint identisch.

Nachfolgefragen. Entscheiden Sie, ob Promise.any oder Promise.race angewendet werden soll, und erklären Sie, was mit den Promises passiert, die nicht gewinnen.

Bewertungskriterien. Geschicklichkeit bei der Verwendung von Promise-Combinatoren sowie ein Verständnis für die Auswirkungen von Fehlern.

Die doppelt abgebuchte Zahlung

Szenario. Die Abrechnung läuft am Zahlungsgateway ab. Die Frontend-Anwendung versucht es erneut – der Kunde wird zweimal belastet.

Frage. Welche Wiederholungsstrategie ist für einen Zahlungsendpunkt sicher?

Antwort. Wiederholungen sind nur bei idempotenten Operationen sicher. Ein POST-Anfragen zur Erstellung einer Gebühr ist standardmäßig nicht idempotent – machen Sie dies durch eine vom Client generierte Idempotenzschlüssel möglich, anhand derer der Server Duplikate entfernt. Wiederholen Sie nur bei Übertragungsfehlern sowie 5xx/429-Antworten – niemals bei 4xx-Fehlern. Trennen Sie die Versuche durch exponentielles Backoff sowie Jitter, damit ein wiederhergestellter Server nicht überlastet wird. Berücksichtigen Sie die Retry-After-Header und setzen Sie eine Obergrenze für die Anzahl der Versuche.

async function postWithRetry(url, body, key, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status < 500 && res.status !== 429) throw new HttpError(res);

    const backoff = 2 ** i * 300 + Math.random() * 300; // jitter
    await new Promise((r) => setTimeout(r, backoff));
  }
  throw new Error('Payment could not be confirmed');
}

Falscher Ansatz. Blindes Dreifachversuchen bei einer festen Zeitvorlage. Timeout-Situationen können zu doppelten Abbuchungen führen, 4xx-Fehler führen nie zum Erfolg, und Ausfälle verstärken sich zu Überlastungen.

Nachbereitung. Der Browser hat die Zeitüberschreitung erreicht, doch die Abrechnung wurde auf Serverseite abgeschlossen – beschreiben Sie den für den Benutzer sichtbaren Wiederherstellungsweg.

Messbarkeit. Idempotentes Design bei Unsicherheiten auf Clientseite.

Die API, die hängen bleibt anstatt zu fehlen

Szenario. Ein Anbieter hört auf zu antworten, schließt die Verbindungen jedoch nicht. fetch wird niemals abgeschlossen; Handler stapeln sich; Ladeindikatoren laufen mehrere Minuten lang.

Frage. Wie begrenzt man das, und was fällt außerhalb einer Zeitüberschreitung noch dazu?

Antwort. Browser geben fetch keinen Standard-Timeout – man muss einen angeben. AbortSignal.timeout ist der moderne Ansatz; AbortSignal.any kombiniert Timeout mit Benutzerstopp. Fügen Sie einen „Circuit Breaker“ hinzu: Nach aufeinanderfolgenden Fehlern sollte der Aufruf vorübergehend gestoppt und sofort eine Alternative bereitgestellt werden, um sowohl die Anwendung als auch die wiederherstellende Abhängigkeit zu schützen.

const signal = AbortSignal.any([
  AbortSignal.timeout(3000),
  userController.signal,
]);

try {
  const res = await fetch(url, { signal });
  breaker.recordSuccess();
  return res.json();
} catch (err) {
  breaker.recordFailure();
  if (err.name === 'TimeoutError') return cachedFallback();
  throw err;
}

Falscher Ansatz. Man setzt einen Timer zum Wettlaufen mit fetch und tut so, als wäre der Verlierer gestoppt. Der HTTP-Aufruf läuft weiter, hält Sockets besetzt und kann trotzdem den Zustand verändern, wenn er schließlich abgeschlossen ist.

Nachfolgemaßnahmen. Setzen Sie Timouts sowohl im Browser als auch am API-Gateway sowie bei Node-upstream-Aufrufen zum selben Anbieter, ohne unerledigte Aufgaben zurückzulassen.

Messbarkeit. Standardmäßiges Misstrauen gegenüber Abhängigkeiten sowie echter Stopp im Vergleich zu unerledigten Aufgaben.

Der Speicherbedarf steigt bei jedem Routenwechsel

Szenario. Ein den ganzen Tag über geöffnetes Support-Tool erreicht 1,4 GB. Heap-Snapshots zeigen, dass getrennte DOM-Node mit jeder Ticket-Navigation zunehmen.

Frage. Übliche Ursachen und wie kann man das überprüfen?

Antwort. Getrennte Node bleiben aktiv, weil etwas weiterhin auf sie verweist: window/document-Listener wurden nie entfernt, setInterval läuft weiter, IntersectionObserver/ResizeObserver wurden nie getrennt, globale Store-Listener sowie Closures in lang lebenden Caches speichern weiterhin den DOM. Überprüfen lässt sich das durch das Erstellen eines Heap-Snapsshots, Navigieren, Auslösen der Garbage Collection, erneutes Erstellen eines Snapsshots und anschließendes Durchgehen der Retainer, bis der Träger offensichtlich wird.

useEffect(() => {
  const onResize = () => recalcLayout();
  const observer = new ResizeObserver(onResize);
  const id = setInterval(pollTicket, 5000);

  window.addEventListener('resize', onResize);
  observer.observe(panelRef.current);

  return () => {
    window.removeEventListener('resize', onResize);
    observer.disconnect();
    clearInterval(id);
  };
}, []);

Falscher Ansatz. Die lokalen Variablen werden bei der Aufräumarbeit auf Null gesetzt, in der Annahme, dass dadurch der Speicherbedarf sinkt. Die Erreichbarkeit der Daten fließt weiterhin über lebende Zuhörer und Timer, die auf dieselben Daten verweisen.

Nachfolgefragen. Ein Leckmuster wird als solches bezeichnet, wenn WeakMap sauber funktioniert, während eine Situation vorliegt, in der schwache Referenzen nicht die richtige Lösung sind.

Messungen. Praktische Heap-Debugging-Methoden sowie Kenntnisse zur Erreichbarkeit von Daten.

Der Zähler, der immer null anzeigt

Szenario. Ein Widget prüft alle fünf Sekunden den Zustand und fügt Benachrichtigungen hinzu. Es zeigt stets nur eine Benachrichtigung an; ein Log innerhalb des Intervalls gibt den ursprünglichen Zustand ewig aus.

Frage. Warum erkennt das Intervall den veralteten Zustand, und wie lässt sich das beheben?

Antwort. Die Funktion wurde einmal mit [] ausgeführt, wodurch die Callback-Funktion den Zustand der ersten Renderung berücksichtigte. Aktualisierungen erzeugen neue Werte; die alte Callback-Funktion verweist weiterhin auf den alten Zustand. Verwenden Sie lieber funktionale Aktualisierer, damit React den neuesten wartenden Wert bereitstellt. Wenn die Callback-Funktion auf Daten angewiesen ist, die ein Aktualisierer nicht liefern kann, spiegeln Sie diese bei jeder Renderung in einen ref wider.

// Broken: `alerts` is frozen at the first render
useEffect(() => {
  const id = setInterval(() => setAlerts([...alerts, poll()]), 5000);
  return () => clearInterval(id);
}, []);

// Fixed: functional update, no stale capture
useEffect(() => {
  const id = setInterval(() => setAlerts((prev) => [...prev, poll()]), 5000);
  return () => clearInterval(id);
}, []);

Falscher Ansatz. alerts als Abhängigkeit der Funktion aufzulisten. Veraltete Callback-Funktionen verschwinden zwar, doch der Zeitintervall wird bei jeder Ergänzung neu gestartet, wodurch das fünfsekündige Intervall zusammenbricht.

Nachfrage. Wie könnte ein benutzerdefinierter Hook ein fünfsekündiges Abfraintervall beibehalten, während stets frischer Zustand gelesen wird?

Messung. Zeitreisende Callback-Funktionen – ein klassisches Problem auf mittlerer Ebene in React.

Zwanzigtausend Zeilen im DOM

Szenario. Eine Inventar-Tabelle zeigt jedes API-Eintrag an. Das Erstellen der Darstellung dauert sechs Sekunden; das Filtern verursacht Verzögerungen; die Tabelle verbraucht Hunderte von Megabyte.

Frage. Wie kann die Tabelle nutzbar gemacht werden, und was sollte zuerst gemessen werden?

Antwort. Zuerst analysieren: Script, Stil, Layout und Darstellung. Bei Tabellen dieser Größe spielt in der Regel die Anzahl der DOM-Node eine entscheidende Rolle. Virtualisieren Sie die Liste, sodass im DOM nur der Sichtbereich zusammen mit einem kleinen Überblicks-Puffer vorhanden ist. Kombinieren Sie dies mit stabilen Zeilenidentitäten, sorgfältiger Memoisierung sowie Filtern und Sortierungen auf Serverseite, sobald die Datensätze auf der Clientseite zu umfangreich werden.

// Row identity matters as much as row count
{visibleRows.map((row) => (
  <Row key={row.id} data={row} />   // stable id, not the array index
))}

Falscher Ansatz. Die Zeilen mit React.memo zu versehen und dabei aufzuhören. Neue inline-Properten heben die Memoisierung auf, und Layout sowie Darstellung werden weiterhin durch Zehntausende von Node überwältigt.

Nachfolgefrage. Was geht schief mit der Fokussierung und den kontrollierten Eingaben, wenn die Zeilenindizes Array-Indizes sind und eine mittlere Zeile entfernt wird?

Messung. Gewohnheiten des Erst-Messen-Lassens und die realistischen Grenzen der Memoisierung.

Von KI

Layout-Probleme durch Filter/Anpassungs-Schleifen

Szenario. Nach dem Laden der Daten passt ein Dashboard die Diagramm-Container an. Die Profile zeigen lange, lila Style-/Layout-Blöcke, die sich in einem Frame Hunderte Male wiederholen.

Frage. Welches Muster verursacht das, und wie lässt es sich beheben?

Antwort. Layout-Probleme. Der Zugriff auf Geometrie-APIs wie Höhenabstände, Begrenzungsrechtecke oder Scrollpositionen zwingt zu einem synchronen Aktualisieren von Stil und Layout, damit der Engine präzise antworten kann. Wenn solche Lesevorgänge mit Schreibvorgängen zu Stilen innerhalb einer Schleife abgewechselt werden, entsteht bei jeder Iteration diese Aktualisierungskosten. Sammeln Sie zunächst die Messwerte ein, ändern Sie anschließend die Stile und planen Sie die Schreibvorgänge mit requestAnimationFrame ein. Für Logik zum Anzeigen/Verbergen nutzen Sie IntersectionObserver, damit die Sichtbarkeit asynchron erreicht wird, ohne das Layout zu beeinflussen.

// Broken: read, write, read, write
panels.forEach((p) => { p.style.height = p.offsetHeight * 1.2 + 'px'; });

// Fixed: batch reads, then batch writes
const heights = panels.map((p) => p.offsetHeight);
requestAnimationFrame(() => {
  panels.forEach((p, i) => { p.style.height = heights[i] * 1.2 + 'px'; });
});

Falscher Ansatz. Jeden Schreibvorgang in einem eigenen Animationenframe planen, während die Lesevorgänge weiterhin gemischt ablaufen. Dadurch werden die Kosten über mehrere Frames verteilt und das Ruckeln hält länger an.

Nachfrage. Welche CSS-Eigenschaften bleiben beim Kompositor, und ab wann verursacht will-change mehr Kosten, als er einspart?

Messung. Die Kosten des Browser-Pipelines als Abfolge – kein Rätselkasten.

Das Warten auf eine synchrone Berechnung befreit den Event-Loop nicht.

Fortgeschritten – Sicherheit, Node, Testing und Design

Auf Führungsebene geht es weniger um die einzige richtige Lösung und mehr um Kompromisse, Auswirkungen sowie Verantwortlichkeiten.

Das CMS-Feld, das ein Skript ausführte

Szenario. Das Marketing speichert Rich-Text in einem headless CMS. Eine Sicherheitsprüfung zeigt <img onerror=...> auf jeder Produktseite.

Frage. Wie ist das passiert, und wie lässt sich das in der gesamten Architektur beheben?

Antwort. Der Wert wird ohne Sanierung in innerHTML oder Reacts dangerouslySetInnerHTML eingefügt. Standardmäßig werden Schleifen durch Escape-Verfahren geschützt ({value} in React, textContent im DOM). Wenn HTML erforderlich ist, sollte man mit einer gepflegten Allowlist-Bibliothek wie DOMPurify sanieren – auch auf der Serverseite, da der Client keine zuverlässige Grenze darstellt. Fügen Sie außerdem eine Content Security Policy hinzu, damit fehlerhafte Inhalte keine inline-Skripte ausführen können.

import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(cmsHtml, {
  ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'li'],
  ALLOWED_ATTR: ['href', 'title'],
});
<div dangerouslySetInnerHTML={{ __html: clean }} />

Falscher Ansatz. Das Löschen von <script>-Tags mithilfe von Regex. Attribut für Event-Handler, javascript:-URLs, SVG-Vektoren sowie verschachtelte Kodierungen bleiben unberührt.

Nachfolgefrage. Analytics fügt inline-Skripte ein, die von der CSP blockiert werden – stellen Sie das Tag wieder her, ohne unsafe-inline zu aktivieren.

Messung. Mehrschichtige XSS-Schutzmaßnahmen mit Sanitierung zum Zeitpunkt der Darstellung.

Das Token in localStorage

Szenario. Nach einem XSS-Angriff notieren die Sicherheitsteams die Sitzungstoken in localStorage, die durch einen Axios-Interceptor hinzugefügt werden. Sie benötigen einen Plan.

Frage. Welche Abwägungen gibt es zwischen localStorage und Cookies für Authentifizierungstoken – und welche Empfehlung gibt es?

Antwort. Skripte auf der Quellseite können localStorage lesen, sodass ein einziger XSS-Angriff die gesamte Sitzung exportieren kann. HttpOnly-Sicherheitscookies mit SameSite=Lax bleiben für JavaScript unsichtbar, doch die Browser fügen sie automatisch hinzu; daher benötigen Änderungsanfragen weiterhin CSRF-Schutzmaßnahmen. Ein gängiges Design: ein kurzlebiger Zugriffstoken im Speicher, ein Erneuerungstoken in einem HttpOnly-Cookie, CSRF-Tokens oder SameSite-Einstellungen bei Änderungen sowie regelmäßige Erneuerung der Tokens. Nichts bleibt bei einem XSS-Angriff unberührt – die Prävention von XSS bleibt weiterhin von größter Bedeutung.

// Server side, Express
res.cookie('refresh_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  path: '/auth/refresh',
  maxAge: 1000 * 60 * 60 * 24 * 7,
});

Falscher Ansatz. Das Verschlüsseln von Tokens in localStorage mit einem Schlüssel, der ebenfalls im Seiten-JavaScript vorhanden ist. Jeder, der den Speicher lesen kann, kann auch den Schlüssel einsehen – reine Scheinlösung.

Nachfrage. Wenn Zugriffstoken nur im Speicher gespeichert sind, wie erhält ein neu geöffnetes Tab eine Sitzung?

Messungsergebnis. Wahl von Token-Speicherlösungen basierend auf Bedrohungsmodellen statt bloßer Slogans.

Der Node-Endpunkt, der alle anderen Endpunkte verlangsamt

Szenario. Ein Express-PDF-Berichtsverarbeiter führt bei Aufruf dazu, dass unzusammenhängende Health-Checks ablaufen und der p99-Wert im gesamten Service ansteigt.

Frage. Warum beeinflusst ein Endpunkt alle anderen – und wie lässt sich das beheben?

Antwort. Node führt die Anwendungs-JavaScript-Code auf einem Thread aus. Rechenintensive Aufgaben blockieren den Event-Loop – es können keine weiteren Anfragen, Timer oder IO-Aufrufe abgewickelt werden. Verlagern Sie rechenintensive Aufgaben auf worker_threads oder eine externe Warteschlange, damit der HTTP-Handler nur Aufgaben in die Warteschlange gibt und darauf reagiert. Streamen Sie große Datenmengen anstelle von Puffern. Wählen Sie asynchrone Krypto-APIs statt Sync-Varianten – verwenden Sie niemals readFileSync auf einer Anfrage-Route.

import { Worker } from 'node:worker_threads';

app.post('/reports', async (req, res) => {
  const job = await queue.add('generate-report', req.body); // returns immediately
  res.status(202).json({ jobId: job.id, status: 'queued' });
});

// Or for in process CPU work
const worker = new Worker('./report-worker.js', { workerData: params });

Falscher Ansatz. Man wartet auf arbeitsschwere CPU-Aufgaben in der Hoffnung, dass der Event-Loop weiterarbeiten kann. Bei Skalierung werden mehr Instanzen genutzt, die alle immer noch bei derselben Art von Anfrage stecken bleiben.

Nachforschung. Welche Produktionsindikatoren deuten bereits vor Beschwerden der Kunden auf einen blockierten Node-Event-Loop hin?

Messung. Trennung von I/O-intensiven und CPU-intensiven Aufgaben, ergänzt durch entsprechende Produktionsindikatoren.

Falscher Inhalt bei Laden (Hydratierung)

Szenario. Eine Next.js-Anwendung zeichnet „Anmelden“ auf dem Server aus und wechselt nach dem Laden zum Benutzernamen. Es treten Warnungen wegen Unstimmigkeiten bei der Hydratierung auf; manchmal heißt es Der Textinhalt stimmt nicht mit dem vom Server generierten HTML überein.

Frage. Was verursacht solche Unstimmigkeiten bei der Hydratierung, und wie lässt sich dieses Problem beheben?

Antwort. Der Server verfügt nicht über browser-spezifische Zustände wie clientseitig gelesene Cookies, localStorage, window.matchMedia oder Date.now(). Hydration erwartet, dass die erste Darstellung auf der Clientseite dem Serverbaum entspricht. Bei einer Abweichung verwirft React die Server-Markup-Struktur für diesen Unterbaum und gibt einen Warnhinweis aus. Um die Sitzung während der Serverdarstellung sichtbar zu machen, müssen die Cookies auf dem Server gelesen und deren Werte in den Baum übernommen werden. Für Werte, die tatsächlich nur im Browser existieren, sollte ein stabiler Platzhalter angezeigt werden, der nach dem Einbinden aktualisiert wird.

// Server component reads the session, so no mismatch
export default async function Layout({ children }) {
  const session = await getSession(cookies());
  return <AuthProvider value={session}>{children}</AuthProvider>;
}

Falscher Ansatz. Das Unterdrücken der Hydration-Warnungen mithilfe eines Wrapper oder die Dynamisierung des Headers, um SSR zu umgehen. Die Abweichung bleibt bestehen, wodurch die Vorteile von SSR verschwinden.

Nachfrage. Wie können relative Zeitstempel auf dem Server angezeigt werden, ohne dass Hydration mit der Client-Uhr kollidiert?

Messungsergebnis. Die Hydratation richtig beheben anstatt Warnungen zu unterdrücken.

Der Cache, der niemals loslässt

Szenario. Eine Charting-Bibliothek speichert Canvas-Elemente, die an DOM-Elemente gebunden sind. Der Speicherbedarf steigt weiterhin an, nachdem die Charts die Seite verlassen haben. Schnappschüsse zeigen Puffer, die von einem Map aufbewahrt werden.

Frage. Warum werden die Einträge nicht entfernt, und wie verändert WeakMap das Ergebnis?

Antwort. Der Garbage Collector kann von den Wurzeln aus erreichbare Objekte bereinigen. Normale Map-Einträge belegen sowohl Schlüssel als auch Wert, sodass ein vom DOM getrennter Knoten, der als Schlüssel verwendet wird, seinen Canvas-Puffer weiterhin erreichbar hält. Ein WeakMap hält die Schlüssel schwach – wenn kein anderes Objekt auf das Element verweist, kann der Eintrag zusammen mit ihm verschwinden. WeakMap ist nicht auflistbar und hat keine Größenangabe; dieser Kompromiss macht die Schwäche sicher.

// Leaks: the Map keeps removed nodes alive
const cache = new Map();

// Collectable: entry dies with the element
const cache = new WeakMap();
cache.set(chartEl, renderedCanvas);

Falscher Ansatz. Cron-ähnliche Überprüfungen, die Cache-Einträge löschen, deren Knoten das Dokument verlassen haben. Das funktioniert, solange man an diese Überprüfungen denkt; WeakMap übernimmt diese Aufgabe.

Nachfolgefragen. Wann ist FinalizationRegistry geeignet, und warum darf die Korrektheit niemals davon abhängen, wann es ausgeführt wird?

Messung. GC als Erreichbarkeitsprüfung, ohne zu behaupten, die Sammelzeit sei steuerbar.

Der Test, der alle zwanzig Ausführungen einmal fehlschlägt

Szenario. Ein Test für ein Suchkomponente funktioniert lokal, scheitert aber in der CI bei etwa 5 % der Fälle beim Suchen nach dem Text „Samantha“. Jemand hat bereits waitFor(3000) hinzugefügt, sodass die CI es erneut versucht.

Frage. Wie kann der Test zuverlässig gemacht werden, und was sagt die Unzuverlässigkeit aus?

Antwort. Unzuverlässige asynchrone Tests prüfen in der Regel die Zeitabläufe statt das Verhalten. Steuern Sie den Debounce mit falschen Timern, ersetzen Sie HTTP an der Grenze durch Tools wie MSW für stabile Datenpakete und überprüfen Sie mit Abfragen, die auf DOM-Änderungen warten, anstatt eine feste Anzahl von Millisekunden zu warten. Wenn ein willkürlicher Wartezeitraum erforderlich ist, deutet das oft auf einen echten Ressourcenkonflikt im Component hin – der Test gibt somit korrekte Informationen wieder.

test('shows results for the final query', async () => {
  vi.useFakeTimers();
  render(<Search />);

await userEvent.type(screen.getByRole('searchbox'), 'samantha');
  await vi.advanceTimersByTimeAsync(300); // debounce window
  expect(await screen.findByText('Samantha')).toBeInTheDocument();
});

Falscher Ansatz. Wiederholte Ausführungen im CI mit immer längeren Timeout-Werten. Das Signal wird zum Rauschen, und eine dreimal wiederholte Testreihe kann einen später in der Produktion auftretenden Ressourcenkonflikt überdecken.

Nachfrage. Wie könnte ein Test dafür sorgen, dass die ältere Suchantwort nach der neueren ankommt?

Messbarkeit. Das Unzuverlässige als Signal interpretieren und asynchrone Tests deterministisch gestalten.

Siebzig Stellen, an denen fetch aufgerufen wird

Szenario. Eine vierjährige Codebasis ruft in sechzig Komponenten fetch auf. Die Fehlerbehandlung ist inkonsistent; die Authentifizierungsaktualisierung wurde elfmal kopiert und eingefügt; niemand kennt die täglichen Timeout-Werte.

Frage. Wie lässt sich eine gemeinsame Datenzugriffsschicht entwerfen und wie erfolgt die Migration ohne Stillstand?

Antwort. Führen Sie gemeinsame Aufgaben in einen einzigen Client zusammen: Basis-URL und Header, eine einheitliche Authentifizierungsaktualisierung, sodass eine Welle von 401-Fehlern nur einen Neustart auslöst, Timeouts, ausschließlich idempotente Wiederholungsversuche, typisierte Fehlernormalisierung sowie Telemetrie-Verbindungen. Halten Sie die Oberfläche klein, damit die Adoption den Umgehungsweg überwiegt. Migrieren Sie schrittweise – liefern Sie zunächst den Client, führen Sie die am stärksten genutzten und fehleranfälligsten Pfade um, verbieten Sie mit Lint-Tools den Einsatz von rohem fetch im neuen Code, damit sich die Grenzen nicht weiter verschieben.

type ApiError =
  | { kind: 'network' }
  | { kind: 'timeout' }
  | { kind: 'http'; status: number; body: unknown }
  | { kind: 'parse' };

export async function apiRequest<T>(
  path: string,
  init: RequestInit & { timeoutMs?: number } = {},
): Promise<{ ok: true; data: T } | { ok: false; error: ApiError }> {
  // timeout, auth, retry, telemetry all live here
}

Falscher Ansatz. Der Vorschlag einer Migration in ein neues Framework oder das Zu fest Einpacken der Antworten, sodass ungewöhnliche Fälle zum direkten fetch führen. Beide Wege erzeugen zwei konkurrierende Datenschichten.

Nachfolgefragen. Sechs Monate später: Wie verhindern Lint-Regeln und Code-Reviews, dass das direkte fetch wieder Einzug hält?

Messbarkeit. API-Design auf Teamebene sowie schrittweise Migration unter Lieferdruck.

Fazit – Vorbereiten ohne Auswendiglernen

Die Vorbereitung auf Trivia hat ihre Grenzen: Auswendig gelernte Zwangstabellen, unerklärlich einfrierende Dashboards. Die Vorbereitung auf Szenarien weist einen anderen Fehlermodus auf: Das Verständnis der Handlung ohne Kenntnis des Mechanismus. Beides lässt sich vermeiden, indem man mit echtem Code arbeitet.

Erzeugen Sie die Fehler absichtlich. Senden Sie ein Suchfeld, das unter Netzwerkbeschränkungen überlastet wird, und öffnen Sie anschließend die Bereiche „Leistung“ und „Netzwerk“, bis die Verzögerungen offensichtlich werden. Lassen Sie einen Zeitraum unberücksichtigt, wechseln Sie zum nächsten Menüpunkt und suchen Sie im Speicher nach den betroffenen Strukturen. Praktische Anleitungen aus den DevTools sind nützlicher als jede Schritt-für-Schritt-Anleitung.

Erfahren Sie sich darin, Messungen durchzuführen, bevor Sie vorgefertigte Antworten geben. Die Panels „Leistung“, „Speicher“ und „Netzwerk“ in Chrome sowie der React Profiler verwandeln viele „fortgeschrittene“ Fragen in gewöhnliche Messfragen.

Üben Sie, die damit verbundenen Kompromisse laut auszusprechen. Fast jedes Szenario lässt sich auf mehr als eine vertretbare Weise lösen, wobei jede Lösung unterschiedliche Kosten mit sich bringt. Gute Interviewer erkennen, ob die Wahl absichtlich getroffen wurde.

Lesen Sie Berichte über Produktionsprobleme. Jeder, der bereits Produkte veröffentlicht hat, kennt fünf solcher Szenarien. Solche eigenen Erfahrungen sind besser als übernommene Beispiele, da daraus unbegrenzt tiefe Nachforschungen möglich sind.

Führen Sie eine kurze Liste von Fehlern mit Ursachen und Lösungen – Notizen, kein Portfolio. Wenn nach einem schwerwiegenden Fehler gefragt wird, ist eine genaue Diagnose immer besser als allgemeine Erklärungen.

Von Anfängern bis zu Fortgeschrittenen wiederholt sich das Muster: Nennen Sie den Fehlermodus, schlagen Sie eine Lösung vor, die zur Bedrohung passt, und wissen Sie, was dazu führt, dass eine falsche Lösung unter Druck attraktiv erscheint. Das ist die Fähigkeit, nach der Produktionsinterviews tatsächlich suchen – nicht eine perfekte Zwangstabelle, sondern die Fähigkeit, Systeme ehrlich zu halten, wenn Netzwerke lügen, der Speicher wächst und Lieferanten versagen.

Teams, die auf diese Weise Interviews durchführen, neigen auch dazu, bessere Postmortems durchzuführen: Das gleiche Vokabular (Races, Thrash, Idempotenz, Blast Radius) taucht sowohl in den Einstellungsprozessen als auch bei der Analyse von Vorfällen auf. Das Studium dieser Szenarien lohnt sich daher doppelt – einmal im Interviewraum und ein weiteres Mal, wenn das Dashboard für zwei Sekunden einfriert, während der Ladebalken völlig still bleibt.

Interviewprozesse, die auf tatsächlichen Produktgeschichten beruhen, zeigen außerdem Kommunikationsfähigkeiten. Wenn jemand eine Situation beschreibt, ohne in Fachjargon zu versinken, ein schnelles Sequenzdiagramm für AbortController zeichnet oder erklärt, warum allSettled den Blast Radius verringert, zeigt das, wie er sich in einem Incidents-Channel verhalten wird. Antworten auf Alltagsfragen belegen selten diese Fähigkeiten, während Antworten auf Szenarien dies fast immer tun – genau deshalb verbreitet sich dieses Format weiterhin in den Einstellungsprozessen für Mittel- und Führungskräfte.