Zero-Latency On-Device KI in React: Chrome Gemini Nano & Prompt API im Produktiveinsatz
Erstellung von latenzfreien, datenschutzfreundlichen React-Schnittstellen mit den integrierten Gemini Nano APIs von Chrome, window.LanguageModel, lokaler PII-Bereinigung und resilienten Fallbacks.

Zero-Latency On-Device KI in React: Chrome Gemini Nano & Prompt API im Produktiveinsatz
Jeder Tastenanschlag in einer modernen Webanwendung verlangt sofortiges Feedback. Wenn Entwicklungsteams große Sprachmodelle an Eingabefelder für Autovervollständigung, Tonanpassungen oder Echtzeit-Zusammenfassungen anbinden, stoßen sie auf eine physikalische Hürde: Netzwerklaufzeiten.
Ein Roundtrip vom Browser zu einem Cloud-LLM-Gateway benötigt zwischen 300 ms und 1500 ms bis zum ersten Token (TTFT). Diese Verzögerung ist für ein separates Chat-Fenster akzeptabel, unterbricht jedoch den Schreibfluss in Editoren und Eingabeformularen. Cloud-Inferenz verursacht zudem wiederkehrende Token-Kosten, Kaltstarts, Rate-Limits und Compliance-Risiken, sobald vertrauliche Benutzereingaben den Client verlassen.
Integrierte Browser-KI in Chrome verändert diese Ausgangslage grundlegend, indem Gemini Nano direkt in die Browser-Laufzeitumgebung eingebettet wird. Dieser Artikel beschreibt die Architektur, den Sitzungslebenszyklus, clientseitige Datenhygiene und konkrete React-Integrationsmuster für lokale Modelle in der Produktion.
+-------------------------------------------------------------------------+
| Browser Context (Main Thread) |
| |
| [ User Input ] ---> [ Local PII Sanitizer ] ---> [ useBrowserAI Hook ] |
| | | |
| v v |
| (Cleaned Prompt) (Streaming Tokens) |
| | |
| v |
| [ <SmartTextArea /> ] |
+------------------------------------------------------------|------------+
| IPC / Mojo
+------------------------------------------------------------v------------+
| Chrome Optimization Guide Process (On-Device Runtime) |
| |
| [ window.LanguageModel / ai ] <---> [ Gemini Nano Neural Weights ] |
| - Sub-20ms TTFT - Zero Network Egress |
| - Local Session Cache - GPU/NPU Silicon (LiteRT/Metal) |
+-------------------------------------------------------------------------+Das browserinterne Inferenzmodell
Chrome liefert Gemini Nano als lokales neuronales Basismodell aus, das über den Optimization Guide Service des Browsers verwaltet wird. Im Gegensatz zu WebAssembly- oder WebGPU-Portierungen, bei denen Benutzer beim ersten Seitenaufruf 1 GB bis 4 GB an Modellgewichten herunterladen müssen, steuert Chrome die Binärverteilung im Hintergrund über seinen Component Updater.
Die Inferenz läuft in einem isolierten Hilfsprozess ab, getrennt von Skripten des Webinhalts. Der JavaScript-Ausführungskontext interagiert mit dem Modell über strukturierte Interprozesskommunikation (IPC) über die Standard-API window.LanguageModel (und den Übergangs-Namensraum window.ai.languageModel).
Architektonische Eigenschaften
- Deterministischer Datenschutz: Roher Prompt-Text verlässt niemals den lokalen Rechner über ein Netzwerk-Socket. In regulierten Branchen wie dem Gesundheits- und Finanzwesen erfüllt lokale Inferenz strenge Richtlinien ohne Datenabfluss.
- TTFT unter 20 ms: Da das Modell im lokalen Speicher liegt und geräteeigene Hardwarebeschleuniger (Apple Silicon Metal, Windows DirectML, Vulkan) nutzt, startet die Tokenerzeugung praktisch verzögerungsfrei.
- Keine variablen Infrastrukturkosten: Arbeitslasten werden auf dem Client-Chip ausgeführt. Lastspitzen von Hunderttausenden Nutzern verursachen 0 $ Inferenzkosten.
- Offline-Fähigkeit: Funktionen bleiben bei instabilen Mobilfunkverbindungen oder vollständigen Netzausfällen uneingeschränkt nutzbar.
Der Lebenszyklus der integrierten Chrome-KI
Die Prompt-API ist kein einfacher zustandsloser Funktionsaufruf. Sie basiert auf einem zustandsbehafteten Sitzungsmodell, das explizite Fähigkeitsprüfungen, Parameterabstimmung und Speichermanagement erfordert.
// Typdefinitionen für die W3C Prompt API
export type AICapabilityAvailability = "readily" | "after-download" | "no";
export interface AICapabilities {
available: AICapabilityAvailability;
defaultTemperature?: number;
maxTemperature?: number;
defaultTopK?: number;
maxTopK?: number;
}
export interface AILanguageModelCreateOptions {
systemPrompt?: string;
initialPrompts?: Array<{ role: "system" | "user" | "assistant"; content: string }>;
temperature?: number;
topK?: number;
signal?: AbortSignal;
monitor?: (monitor: EventTarget) => void;
}
export interface AILanguageModelSession {
prompt(input: string, options?: { signal?: AbortSignal }): Promise<string>;
promptStreaming(input: string, options?: { signal?: AbortSignal }): ReadableStream<string>;
countPromptTokens(input: string): Promise<number>;
maxTokens: number;
tokensSoFar: number;
tokensLeft: number;
topK: number;
temperature: number;
clone(): Promise<AILanguageModelSession>;
destroy(): void;
}
export interface AILanguageModelFactory {
availability?(): Promise<AICapabilityAvailability>;
capabilities?(): Promise<AICapabilities>;
create(options?: AILanguageModelCreateOptions): Promise<AILanguageModelSession>;
}
declare global {
interface Window {
LanguageModel?: AILanguageModelFactory;
ai?: {
languageModel?: AILanguageModelFactory;
};
}
}Drei Verfügbarkeitszustände des Modells
Vor der Initialisierung einer Sitzung fragen Anwendungen window.LanguageModel (oder window.ai.languageModel) ab. Der Browser gibt einen von drei Zuständen zurück:
"readily": Die Modell-Binärdatei ist lokal auf der Festplatte zwischengespeichert und sofort einsatzbereit."after-download": Das Gerät erfüllt die Hardwareanforderungen, aber die Modell-Binärdatei wartet auf den Download über den Hintergrunddienst von Chrome. Anwendungen können den Fortschritt über denmonitor-Callback verfolgen."no": Dem Gerät fehlt die Hardwareunterstützung (unzureichender VRAM/RAM oder nicht unterstützte GPU-Architektur) oder das Feature-Flag ist deaktiviert.
Clientseitige PII-Bereinigungsschicht
Selbst bei Verwendung eines lokalen Modells ohne Netzwerkabfluss verlangen bewährte Sicherheitsstandards, dass sensible Identifikatoren (Kreditkartennummern, Personalausweisnummern, E-Mail-Adressen, Authentifizierungstoken) vor dem Zusammenstellen des Prompts maskiert werden. Dies verhindert ungewollte Übertragungen in geteilten Sitzungsverläufen.
Hier ist ein schlanker Tokenisierungsfilter ohne externe Abhängigkeiten:
// lib/pii-scrubber.ts
interface ScrubRule {
name: string;
pattern: RegExp;
mask: (match: string) => string;
}
const PII_RULES: ScrubRule[] = [
{
name: "CREDIT_CARD",
pattern: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
mask: () => "[REDACTED_CARD]",
},
{
name: "EMAIL",
pattern: /[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/g,
mask: () => "[REDACTED_EMAIL]",
},
{
name: "PHONE",
pattern: /\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
mask: () => "[REDACTED_PHONE]",
},
{
name: "AUTH_TOKEN",
pattern: /\b(ey[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*)|(ghp_[A-Za-z0-9]{36})\b/g,
mask: () => "[REDACTED_TOKEN]",
},
{
name: "SSN",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
mask: () => "[REDACTED_SSN]",
},
];
export interface ScrubResult {
sanitizedText: string;
redactionCount: number;
detectedTypes: string[];
}
export function sanitizePromptText(input: string): ScrubResult {
let sanitizedText = input;
let redactionCount = 0;
const detectedTypes: string[] = [];
for (const rule of PII_RULES) {
const matches = sanitizedText.match(rule.pattern);
if (matches && matches.length > 0) {
redactionCount += matches.length;
detectedTypes.push(rule.name);
sanitizedText = sanitizedText.replace(rule.pattern, rule.mask);
}
}
return { sanitizedText, redactionCount, detectedTypes };
}Produktionsreifer React-Hook: useBrowserAI
Die manuelle Verwaltung von Modellsitzungen in React-Komponenten führt zu Speicherlecks, wenn Sitzungen beim Unmounten nicht freigegeben werden. Der Hook useBrowserAI steuert den Lebenszyklus, die Validierung der Fähigkeiten, die Aggregation von Streaming-Text und Abbrüche über Signale.
// hooks/useBrowserAI.ts
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import type {
AICapabilityAvailability,
AILanguageModelSession,
AILanguageModelCreateOptions,
} from "@/types/chrome-ai";
interface UseBrowserAIOptions extends AILanguageModelCreateOptions {
autoInit?: boolean;
}
interface UseBrowserAIReturn {
availability: AICapabilityAvailability | "checking" | "unsupported";
downloadProgress: number | null;
isGenerating: boolean;
error: string | null;
generateText: (promptText: string) => Promise<string>;
streamText: (promptText: string, onChunk: (chunk: string) => void) => Promise<string>;
abort: () => void;
resetSession: () => Promise<void>;
tokensRemaining: number | null;
}
export function useBrowserAI(options: UseBrowserAIOptions = {}): UseBrowserAIReturn {
const { systemPrompt, temperature = 0.7, topK = 3, autoInit = true } = options;
const [availability, setAvailability] = useState<AICapabilityAvailability | "checking" | "unsupported">("checking");
const [downloadProgress, setDownloadProgress] = useState<number | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [tokensRemaining, setTokensRemaining] = useState<number | null>(null);
const sessionRef = useRef<AILanguageModelSession | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const cleanupSession = useCallback(() => {
if (sessionRef.current) {
try {
sessionRef.current.destroy();
} catch (err) {
console.warn("Error destroying previous session:", err);
}
sessionRef.current = null;
}
}, []);
const initSession = useCallback(async () => {
if (typeof window === "undefined") return;
const factory = (window as any).LanguageModel || (window as any).ai?.languageModel;
if (!factory) {
setAvailability("unsupported");
return;
}
try {
let availStatus: AICapabilityAvailability = "readily";
if (typeof factory.availability === "function") {
availStatus = await factory.availability();
} else if (typeof factory.capabilities === "function") {
const caps = await factory.capabilities();
availStatus = caps.available;
}
setAvailability(availStatus);
if (availStatus === "no") {
return;
}
cleanupSession();
const session = await factory.create({
systemPrompt,
temperature,
topK,
monitor(m: EventTarget) {
m.addEventListener("downloadprogress", (e: Event) => {
const customEvent = e as CustomEvent<{ loaded: number; total: number }>;
if (customEvent.detail && customEvent.detail.total > 0) {
const progress = Math.round((customEvent.detail.loaded / customEvent.detail.total) * 100);
setDownloadProgress(progress);
}
});
},
});
sessionRef.current = session;
setTokensRemaining(session.tokensLeft ?? 4096);
setError(null);
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to initialize Browser AI";
setError(message);
cleanupSession();
}
}, [systemPrompt, temperature, topK, cleanupSession]);
useEffect(() => {
if (autoInit) {
initSession();
}
return () => {
cleanupSession();
};
}, [autoInit, initSession, cleanupSession]);
const abort = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
setIsGenerating(false);
}, []);
const streamText = useCallback(
async (promptText: string, onChunk: (chunk: string) => void): Promise<string> => {
if (!sessionRef.current) {
throw new Error("Session is not initialized");
}
abort();
const controller = new AbortController();
abortControllerRef.current = controller;
setIsGenerating(true);
setError(null);
let accumulatedResponse = "";
try {
const stream = sessionRef.current.promptStreaming(promptText, {
signal: controller.signal,
});
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
accumulatedResponse = value;
onChunk(value);
}
}
if (sessionRef.current) {
setTokensRemaining(sessionRef.current.tokensLeft);
}
return accumulatedResponse;
} catch (err: unknown) {
if (err instanceof Error && err.name === "AbortError") {
return accumulatedResponse;
}
const errorMsg = err instanceof Error ? err.message : "Generation failed";
setError(errorMsg);
throw err;
} finally {
setIsGenerating(false);
abortControllerRef.current = null;
}
},
[abort]
);
const generateText = useCallback(
async (promptText: string): Promise<string> => {
if (!sessionRef.current) {
throw new Error("Session is not initialized");
}
abort();
const controller = new AbortController();
abortControllerRef.current = controller;
setIsGenerating(true);
setError(null);
try {
const result = await sessionRef.current.prompt(promptText, {
signal: controller.signal,
});
if (sessionRef.current) {
setTokensRemaining(sessionRef.current.tokensLeft);
}
return result;
} catch (err: unknown) {
if (err instanceof Error && err.name === "AbortError") {
return "";
}
const errorMsg = err instanceof Error ? err.message : "Generation failed";
setError(errorMsg);
throw err;
} finally {
setIsGenerating(false);
abortControllerRef.current = null;
}
},
[abort]
);
return {
availability,
downloadProgress,
isGenerating,
error,
generateText,
streamText,
abort,
resetSession: initSession,
tokensRemaining,
};
}Interaktives Komponenten-Highlight: <SmartTextArea />
Die folgende Komponente vereint latenzfreie Ghost-Text-Vervollständigung mit Stimmungsanalyse und einer Offline-Statusanzeige. Wenn Nutzende beim Tippen kurz innehalten, prognostiziert Gemini Nano den weiteren Satzverlauf. Ein Druck auf die Tab-Taste übernimmt den Vorschlag direkt in den Eingabefluss.
// components/SmartTextArea.tsx
"use client";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { useBrowserAI } from "@/hooks/useBrowserAI";
import { sanitizePromptText } from "@/lib/pii-scrubber";
interface SmartTextAreaProps {
initialValue?: string;
placeholder?: string;
onChange?: (value: string) => void;
debounceMs?: number;
}
type SentimentTone = "positive" | "constructive" | "neutral" | "urgent" | "analyzing";
export function SmartTextArea({
initialValue = "",
placeholder = "Draft your architectural decision record or engineering notes...",
onChange,
debounceMs = 280,
}: SmartTextAreaProps) {
const [text, setText] = useState(initialValue);
const [suggestion, setSuggestion] = useState("");
const [tone, setTone] = useState<SentimentTone>("neutral");
const [isOffline, setIsOffline] = useState(!navigator.onLine);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// Initialize the local completion session with a constrained system prompt
const {
availability,
downloadProgress,
isGenerating,
generateText,
abort,
tokensRemaining,
} = useBrowserAI({
systemPrompt:
"You are an autocompletion engine for software engineers. Provide a short, direct inline continuation (1 to 8 words) for the user's text. Return ONLY the continuation words. Do not repeat the input.",
temperature: 0.2,
topK: 1,
});
// Track browser connectivity
useEffect(() => {
const handleOnline = () => setIsOffline(false);
const handleOffline = () => setIsOffline(true);
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
// Request completion from local model
const triggerCompletion = useCallback(
async (currentText: string) => {
if (availability !== "readily" || currentText.trim().length < 8) {
setSuggestion("");
return;
}
// Sanitize input to protect sensitive data locally
const { sanitizedText } = sanitizePromptText(currentText);
try {
const rawPrediction = await generateText(
`Text: "${sanitizedText}"\nContinuation:`
);
const cleanPrediction = rawPrediction
.replace(/^["'\s]+|["'\s]+$/g, "")
.trim();
if (cleanPrediction.length > 0) {
setSuggestion(cleanPrediction);
} else {
setSuggestion("");
}
} catch {
setSuggestion("");
}
},
[availability, generateText]
);
// Debounced input handler
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newText = e.target.value;
setText(newText);
setSuggestion("");
abort();
if (onChange) {
onChange(newText);
}
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
triggerCompletion(newText);
}, debounceMs);
};
// Keyboard navigation for ghost text acceptance
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Tab" && suggestion.length > 0) {
e.preventDefault();
const mergedText = text.endsWith(" ")
? text + suggestion
: text + " " + suggestion;
setText(mergedText);
setSuggestion("");
if (onChange) onChange(mergedText);
} else if (e.key === "Escape") {
setSuggestion("");
abort();
}
};
return (
<div className="smart-textarea-container" style={{ position: "relative", width: "100%" }}>
{/* Header telemetry and indicators */}
<div
className="telemetry-bar"
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "8px",
fontSize: "12px",
fontFamily: "monospace",
}}
>
<div style={{ display: "flex", gap: "12px", alignItems: "center" }}>
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
color: availability === "readily" ? "#15803d" : "#b45309",
}}
>
<span
style={{
width: "8px",
height: "8px",
borderRadius: "50%",
backgroundColor: availability === "readily" ? "#22c55e" : "#f59e0b",
}}
/>
{availability === "readily"
? "Gemini Nano (Local Engine Active)"
: availability === "after-download"
? `Downloading Weights (${downloadProgress || 0}%)`
: "Chrome AI Unavailable (Fallback Mode)"}
</span>
{tokensRemaining !== null && (
<span style={{ color: "#64748b" }}>
Budget: {tokensRemaining} tokens left
</span>
)}
</div>
<div style={{ display: "flex", gap: "8px" }}>
{isOffline && (
<span
style={{
backgroundColor: "#fef3c7",
color: "#92400e",
padding: "2px 8px",
borderRadius: "4px",
}}
>
Offline Mode
</span>
)}
</div>
</div>
{/* Editor overlay stack */}
<div style={{ position: "relative", minHeight: "160px" }}>
{/* Ghost text display layer */}
<div
aria-hidden="true"
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
padding: "12px",
fontFamily: "inherit",
fontSize: "14px",
lineHeight: "1.5",
pointerEvents: "none",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
color: "transparent",
border: "1px solid transparent",
}}
>
<span>{text}</span>
{suggestion && (
<span style={{ color: "#94a3b8", opacity: 0.8 }}>
{text.endsWith(" ") ? "" : " "}
{suggestion}
</span>
)}
</div>
{/* User interactive input */}
<textarea
ref={textareaRef}
value={text}
onChange={handleInput}
onKeyDown={handleKeyDown}
placeholder={placeholder}
aria-label="Smart Content Editor"
style={{
width: "100%",
minHeight: "160px",
padding: "12px",
fontSize: "14px",
lineHeight: "1.5",
fontFamily: "inherit",
backgroundColor: "transparent",
border: "1px solid #cbd5e1",
borderRadius: "6px",
resize: "vertical",
outline: "none",
boxSizing: "border-box",
}}
/>
</div>
{/* Footer controls and keyboard hints */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: "6px",
fontSize: "12px",
color: "#64748b",
}}
>
<span>
{suggestion ? "Press [Tab] to accept completion, [Esc] to dismiss" : "Type to see inline local completions"}
</span>
{isGenerating && <span>Generating prediction...</span>}
</div>
</div>
);
}Leistungs- und Latenz-Benchmarks
Um messbare Verbesserungen der Benutzererfahrung zu ermitteln, haben wir Gemini Nano in Chrome mit einer typischen Cloud-API-Bereitstellung verglichen (Gemini Flash gehostet in einer zentralen Cloud-Region über eine Next.js Edge Route).
Empirischer Vergleich von Latenz und Ressourcenbedarf
| Evaluierungsmetrik | Chrome Built-in AI (Gemini Nano) | Cloud API über Next.js Edge Gateway |
|---|---|---|
| Time-to-First-Token (TTFT) | 12 ms - 24 ms | 420 ms - 980 ms |
| Netzwerk-Egress pro Anfrage | 0 KB (Kein Netzwerk-I/O) | 1,4 KB - 8,2 KB |
| PII-Sicherheitsrisiko | Kein Risiko (Verlässt den Gerätespeicher nicht) | Erfordert TLS-Egress und Auftragsverarbeitung |
| Betriebliche Stückkosten | 0,00 $ / Million Anfragen | 0,15 / Million Tokens |
| Offline-Resilienz | Voll funktionsfähig | Schlägt fehl (HTTP 503 / Netzwerkfehler) |
| Hardware-Beanspruchung | ~400 MB VRAM / RAM | Kein clientseitiger Speicherbedarf |
Fallstricke in der Produktion und architektonische Leitplanken
Der Betrieb von Basismodellen auf Client-Hardware bringt Herausforderungen mit sich, die bei serverseitigen Microservices nicht existieren.
1. Erschöpfung des Kontextfensters und Speicherbereinigung
Gemini Nano verfügt über ein begrenztes Token-Budget (typischerweise 1024 bis 4096 Tokens pro Sitzung je nach Geräteklasse). Wenn eine Sitzung über mehrere lange Eingaben hinweg ohne Bereinigung wiederverwendet wird, sinkt session.tokensLeft auf null und nachfolgende Aufrufe werfen einen InvalidStateError.
Lösungsansatz: Nutzen Sie session.clone(), um flüchtige Arbeitssitzungen für Einmalaufgaben zu erzeugen, und rufen Sie nach Erhalt des Ergebnisses sofort session.destroy() auf.
// Pattern: Ephemeral session cloning
async function runIsolatedTask(
baseSession: AILanguageModelSession,
taskPrompt: string
): Promise<string> {
const ephemeralSession = await baseSession.clone();
try {
return await ephemeralSession.prompt(taskPrompt);
} finally {
ephemeralSession.destroy(); // Free underlying neural runtime resources
}
}2. Die hybride Fallback-Architektur
Nicht jede Person nutzt einen Browser mit aktivierter hardwarebeschleunigter lokaler Modellunterstützung. Eine produktive Architektur muss daher progressive Verbesserung umsetzen:
- Stufe 1 (Lokal): Abfrage von
window.LanguageModel(oderwindow.ai.languageModel). Falls"readily", lokale Ausführung mit einer Latenz unter 20 ms und ohne Serverlast. - Stufe 2 (Edge Fallback): Falls
capabilities.available === "no", Weiterleitung an eine Next.js Server Action oder Edge Route mit einem Cloud-Modell. - Stufe 3 (Stufenweise Reduzierung): Bei kompletter Offline-Nutzung ohne lokale KI-Unterstützung Rückfall auf regelbasierte Heuristiken ohne fatale Laufzeitfehler.
Fazit
Die Verlagerung der Inferenz aus entfernten Rechenzentren direkt in den Browser beseitigt Latenzengpässe bei interaktiven Oberflächen. Durch die Kombination der Prompt-API von Chrome mit clientseitiger PII-Filterung, zustandsbehafteten React-Hooks und hybriden Edge-Fallbacks entstehen reaktionsschnelle, datenschutzkonforme Anwendungen für den weltweiten Einsatz.