Das schnellste Drittanbieter-Skript ist das, das nie läuft

Die schnellste Variante eines Drittanbieter-Skripts ist die, die nie ausgeführt wird. Ich habe das auf einer Nachrichtenseite mit hohem Traffic neu gelernt, wo die Serverantwort bereits gut war und die Seite sich trotzdem kaputt anfühlte. Das HTML kam schnell an, die Artikelüberschrift stand direkt im Markup, und trotzdem stand die Seite einfach nur da, ruckelig und träge, während der Hauptthread im JavaScript anderer Leute ertrank.
Das hier ist also eine Geschichte über diese Seite. Über die Zahlen, die das Problem aufdeckten, die Muster, die sie von peinlich zu schnell gebracht haben, und die wenigen Stellen, an denen das schnelle Vorgehen mit dem kollidierte, was das Unternehmen eigentlich wollte. Keine der Techniken ist clever. Es geht meist nur darum, sich zu weigern, Dinge zu laden, bevor man einen konkreten Grund dafür hat.
Ein schneller Server ist keine schnelle Seite
Die Seite lief auf einem modernen Stack mit Server-Rendering und einem vernünftigen Cache davor. An einem guten Tag kam das Dokument in ein paar hundert Millisekunden an. Nach der einzigen Kennzahl, die den meisten Backend-Dashboards wichtig ist, war alles in Ordnung.
Es war nicht in Ordnung. Auf einem Android-Mittelklassegerät über gedrosseltes 4G fühlte sich die Seite an wie ein Marsch durch nassen Sand. Tippen tat eine Sekunde lang nichts. Das Scrollen stockte. Der Artikeltitel, der im initialen HTML stand, brauchte den größten Teil von fünf Sekunden, um tatsächlich gerendert zu werden.
Diese Lücke zwischen "der Server ist schnell" und "die Seite fühlt sich schnell an" ist der Ort, an dem Drittanbieter-JavaScript lebt. Time to First Byte misst, wie schnell der Server zu reden beginnt. Es sagt nichts darüber aus, was passiert, nachdem die Bytes angekommen sind, wenn der Browser jedes Skript, das die Seite einbindet, parsen, kompilieren und ausführen muss. Die Kennzahl, die diesen Schmerz erfasst, ist Total Blocking Time, und im Labor ist sie ein brauchbarer Stellvertreter dafür, wie träge sich eine Seite anfühlt, bevor sie sich beruhigt. Im Feld zeigt sich dasselbe Problem als schlechte Interaction to Next Paint.
Die Form des Problems
Das Artikel-Template bettete die übliche Besetzung ein: Twitter/X-Tweets, Instagram-Posts, TikTok-Clips, einen Tag-Manager und einen Analytics-Anbieter. Jeder davon bringt sein eigenes SDK mit, und jedes SDK will in dem Moment ausgeführt werden, in dem die Seite lädt. Der Browser, gehorsam wie er ist, tut genau das.
Als ich das Performance-Panel öffnete und einen typischen Artikel auf einem gedrosselten Profil aufzeichnete, war das Bild düster, und fast nichts davon war mein Code:
- Allein Twitters
widgets.jskostete etwa 816 ms Ausführungszeit auf dem Hauptthread. - Die kombinierten Social-SDKs fügten weit über 1,5 Sekunden blockierende Arbeit hinzu.
- Der Tag-Manager steuerte ganz für sich einen Task von 241 ms bei.
- Der Analytics-Anbieter und ein paar kleinere Skripte legten noch ein paar hundert Millisekunden drauf.
Das Grausame daran ist, dass die meisten dieser Embeds unterhalb der Falz lagen. Eine Leserin am Handy zahlte 800 ms, um einen Tweet zu rendern, zu dem sie vielleicht nie scrollen würde. Das Largest Contentful Paint-Element war ein schlichtes <h1>, das bereits im Server-HTML stand, aber es konnte nicht gerendert werden, während der Hauptthread damit beschäftigt war, Embed-Code zu kompilieren und auszuführen, der nichts mit der Überschrift zu tun hatte.
So sah der Ladevorgang ungefähr aus, bevor irgendetwas davon getan war:
Hier ist der unangenehme Teil: Dein Performance-Budget gehört nicht wirklich dir. Du vermietest den größten Teil davon an Anbieter, und die geben es aus, ohne zu fragen. Jede ernsthafte Lösung muss damit beginnen, dieses Budget zurückzuholen.
Finde die echten Schuldigen, bevor du irgendetwas anfasst
Es ist verlockend, aus dem Bauch heraus Skripte zu löschen. Widersteh dem. Die erste Aufgabe ist die Zuordnung, denn das Skript, das du für das Problem hältst, ist oft nicht das, das dich am meisten kostet.
Zwei Werkzeuge erledigten hier den Großteil der Arbeit. Das Flammendiagramm des Hauptthreads im Performance-Panel zeigt dir genau, welches Skript auf der CPU geparkt ist und wie lange. Gruppiere die Aufzeichnung nach langen Tasks, denen über 50 ms, und die Übeltäter sortieren sich selbst nach oben. Das zweite Werkzeug ist der Attribution-Build der web-vitals-Bibliothek, der dir im Feld nicht nur sagt, dass LCP langsam war, sondern warum, aufgeschlüsselt in Time to First Byte, Resource Load Delay, Resource Load Time und Element Render Delay. Auf dieser Seite war der Render-Delay-Wert enorm, während jeder andere Teil nahe null lag, was direkt auf Konkurrenz um den Hauptthread hindeutete und nicht auf ein langsames Netzwerk oder ein schweres Bild.
Du willst Zahlen wie diese vor und nach jeder Änderung notiert haben. Sonst optimierst du nach Gefühl, und Gefühle überstehen es nicht, wenn ein Stakeholder fragt, ob sich die Arbeit gelohnt hat.
Hör auf zu laden, wonach niemand gefragt hat
Der erste echte Schritt ist auch der wirksamste. Lade ein Embed-SDK nicht, bevor das Embed kurz davor ist, gesehen zu werden. Der Browser liefert das Werkzeug dafür bereits mit: `IntersectionObserver`.
Anstatt die <script>-Tags der Plattform zur Parse-Zeit laufen zu lassen, entferne sie auf dem Server und injiziere einen kleinen Observer, der jedes SDK nur dann lädt, wenn sein Embed dem Viewport nahekommt. Beobachte außerdem jedes Embed einzeln, damit ein Tweet nahe oben nicht das TikTok-SDK ganz unten mit hereinzieht. Ein einziger gemeinsamer Loader, der feuert, sobald irgendein Embed erscheint, verschenkt den größten Teil des Nutzens.
type EmbedKind = "twitter" | "instagram" | "tiktok";
const SDK_URL: Record<EmbedKind, string> = {
twitter: "https://platform.twitter.com/widgets.js",
instagram: "https://www.instagram.com/embed.js",
tiktok: "https://www.tiktok.com/embed.js",
};
const loaded = new Set<EmbedKind>();
function loadSdk(kind: EmbedKind): void {
if (loaded.has(kind)) return; // dasselbe SDK niemals zweimal injizieren
loaded.add(kind);
const s = document.createElement("script");
s.src = SDK_URL[kind];
s.async = true;
s.setAttribute("fetchpriority", "low"); // es kann hinter echten Inhalten warten
document.body.appendChild(s);
}
const io = new IntersectionObserver(
(entries, observer) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const kind = entry.target.getAttribute("data-embed") as EmbedKind;
loadSdk(kind);
observer.unobserve(entry.target); // einmal laden, dann nicht mehr beobachten
}
},
{ rootMargin: "300px" }, // etwas vorher starten, bevor das Embed sichtbar ist
);
document
.querySelectorAll<HTMLElement>("[data-embed]")
.forEach((el) => io.observe(el));Zwei Stellschrauben sind hier wichtig. Der rootMargin von 300px startet das Laden kurz bevor das Embed ins Sichtfeld scrollt, sodass der Inhalt meist bereit ist, wenn die Leserin dort ankommt, statt verspätet aufzupoppen. Und fetchpriority="low" sagt dem Browser, dass diese Anfragen hinter den Schriften, dem CSS und dem LCP-Bild warten können. Die Priority Hints API ist ungefähr so günstig ein Gewinn, wie du ihn finden wirst. Es gibt auch einen Fallback, den man behalten sollte: Wenn IntersectionObserver nicht verfügbar ist, lade die SDKs zeitlich gestaffelt mit kleinen Verzögerungen dazwischen statt alle auf einmal, damit selbst der Pfad für alte Browser den Hauptthread nicht verstopft.
Diese eine Änderung verschob auf embedlastigen Artikeln rund 1,5 Sekunden blockierender Arbeit weg vom kritischen Pfad. Aber sie hat noch einen Fehler, und der Fehler ist eine Annahme.
Wenn faul immer noch zu eifrig ist: die Fassade
Faules Laden beim Scrollen nimmt an, dass die Leserin das Embed will. Viele wollen es nicht. Sie kamen für den Artikel, scrollen am Tweet vorbei und brauchten nie 200 KB Widget-Code dafür. Für die schlimmsten Übeltäter ging ich weiter und ersetzte das echte Embed durch eine Fassade: billiges statisches HTML, das wie das Embed aussieht, wobei das echte SDK nur bei einem Klick oder einer echten Interaktion lädt.
Das ist das Import-on-Interaction-Muster, angewandt auf den Code anderer Leute statt auf deinen eigenen. Hier ist der Lebenszyklus:
Damit das robust wurde, brauchte es drei Lektionen, und zwei davon habe ich auf die harte Tour gelernt.
Die erste Lektion drehte sich darum, woher die Embeds tatsächlich kommen. Das CMS lieferte manche Embeds als <blockquote>-Markup und andere als vollständige <iframe>-Elemente. Meine erste Fassade behandelte nur Blockquotes, also holten sich die iframe-Embeds fröhlich ihre Nutzlast während des HTML-Parsens, und die Fassade brachte nichts. Die Lösung bestand darin, iframes auf dem Server zu neutralisieren, bevor sie je den Browser erreichten, indem man src zu data-src verschob und es erst bei der Aktivierung wiederherstellte.
function activateEmbed(container: HTMLElement): void {
const iframe = container.querySelector<HTMLIFrameElement>("iframe[data-src]");
if (!iframe) return;
const run = () => {
iframe.src = iframe.dataset.src!; // die echte Quelle wiederherstellen
iframe.removeAttribute("data-src");
};
// Im Leerlauf einplanen, damit es nie mit Tippen oder Scrollen konkurriert.
if ("requestIdleCallback" in window) {
requestIdleCallback(run, { timeout: 2000 });
} else {
setTimeout(run, 1);
}
}Die zweite Lektion kostete mich einen Nachmittag. Meine erste Version erzeugte das Fassaden-Markup auf dem Server und versuchte, den Klick-Handler mit einem in das Artikel-HTML injizierten Inline-<script> zu verdrahten. Es tat stillschweigend nichts. React führt <script>-Tags, die über dangerouslySetInnerHTML eingefügt werden, nicht aus, also wurde der Handler nie angehängt und die Embeds luden schlicht nie. Die Fassaden sahen richtig aus und waren völlig tot. Die Lösung war, das Verhalten nicht länger durch HTML-Strings zu schmuggeln und das Ganze in eine echte Client-Komponente zu verlagern, die beim Mounten läuft, die Fassaden findet und ihre eigenen Listener anhängt. Wenn du eine praktische Sache aus diesem Text mitnimmst, dann diese: Verhalten gehört in Komponenten, nicht in HTML, das du als String injizierst.
Die dritte Lektion drehte sich um das Einplanen. Wenn das echte Skript endlich lädt, wickle die Injektion in `requestIdleCallback` ein, damit es sich in einen ruhigen Moment einfügt, statt mit einer Interaktion zu kämpfen, die die Nutzerin gerade jetzt macht. Click-to-Load beim einzelnen schlimmsten Embed entfernte weitere 816 ms aus dem initialen Laden, weil Twitters SDK für die meisten Leser schlicht nie lief.
Wo Schnelligkeit mit dem Geschäft kollidierte
Performance-Arbeit findet nicht im luftleeren Raum statt, und das ist der Teil, den die meisten Berichte überspringen. Zwei meiner "Gewinne" verursachten echte Probleme.
Der erste war der Videoplayer. Ich ersetzte den schweren eingebetteten Player durch ein statisches Poster und einen Play-Button, was für die Ladezeit die richtige Entscheidung ist. Aber die Fassade verbarg den Titel des Videos, und es stellte sich heraus, dass der Titel echte Arbeit leistete: Leser entschieden anhand von ihm, ob sie abspielten. Die Abspielzahlen sanken. Der Kompromiss war, die leichtgewichtige Fassade zu behalten, sie aber eine Sekunde nach der ersten Interaktion der Leserin mit der Seite automatisch zum vollen Player aufzurüsten, eingeplant im Leerlauf. Keine Kosten während des Fensters, das deine Core Web Vitals entscheidet, volle Funktionalität unmittelbar danach.
Das zweite war, dass die Embeds tot blieben, bis man klickte. Die Redaktion fand es nicht gut, dass Leser jeden Tweet anklicken mussten, um ihn zu sehen. Also bekamen die Fassaden dasselbe Verhalten: auf das erste scroll, click, keydown oder touchstart lauschen und eine Sekunde später die übrigen Embeds im Leerlauf still aktivieren. Direkte Klicks funktionieren weiterhin sofort. Das Ergebnis hält den Hauptthread durch das Messfenster hindurch frei und stellt dann in dem Moment, in dem die Leserin irgendein Lebenszeichen gibt, das "alles lädt einfach"-Erlebnis wieder her.
Die Lektion, die ich immer wieder neu lerne: Eine Performance-Lösung, die das Produkt kaputt macht, ist keine Lösung, sondern eine Regression mit guten Kennzahlen. Das interaktionsgesteuerte Aufrüsten ist das Muster, das beides versöhnt.
Die Skripte, die du nicht entfernen kannst
Manche Dinge müssen laden: Analytics, Consent-Management, ein Tag-Manager, von dem das Marketing-Team abhängt. Du kannst sie nicht löschen, aber du kannst dich weigern, sie während des Fensters laufen zu lassen, das zählt.
Für die habe ich Gatter gestapelt. Auf das load-Event warten, dann einen kurzen Timer, dann requestIdleCallback mit einem Timeout, damit es auch auf einer beschäftigten Seite noch feuert. Der Tag-Manager wurde von einem 241-ms-Task, der mit dem ersten Rendern kämpfte, zu einem stillen Job, der läuft, sobald die Seite interaktiv und im Leerlauf ist.
function loadWhenIdle(src: string, delayMs = 3000): void {
const inject = () => {
const s = document.createElement("script");
s.src = src;
s.async = true;
s.setAttribute("fetchpriority", "low");
document.body.appendChild(s);
};
// load-Event -> kurze Verzoegerung -> Leerlauf. Jedes Gatter schiebt die Arbeit weiter nach hinten.
window.addEventListener("load", () => {
setTimeout(() => {
if ("requestIdleCallback" in window) {
requestIdleCallback(inject, { timeout: 8000 });
} else {
inject();
}
}, delayMs);
});
}Der andere Trick für die Skripte, mit denen du feststeckst, ist Caching. Ein Anbieter, der seine Datei mit einem Cache-Header von einem Tag ausliefert, ist ein Anbieter, den du ständig erneut herunterlädst, und über diesen Header hast du keine Kontrolle. Leite das Asset über deine eigene Domain weiter, setze eine vernünftige Cache-Lebensdauer, und aus wiederholten Netzwerkkosten werden nahezu einmalige. Es gibt dir außerdem eine einzige Kehle zum Zudrücken, wenn der Drittanbieter einen Ausfall hat. In einem Framework wie Next.js kannst du den Proxy mit einem Rewrite und einer Cache-Header-Regel umsetzen:
// next.config.ts
const nextConfig = {
async rewrites() {
return [
{ source: "/_proxy/analytics.js", destination: "https://vendor.example/sdk.js" },
];
},
async headers() {
return [
{
source: "/_proxy/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=604800, stale-while-revalidate=31536000",
},
],
},
];
},
};
export default nextConfig;Eine Denkweise für jedes Drittanbieter-Skript
Nach genug von diesen Fällen fallen die einzelnen Tricks in eine einzige Entscheidung zusammen, die du bei jedem Skript durchlaufen kannst, bevor du es an eine Seite lässt.
Die meisten Skripte kommen nie über die zweite Frage hinaus, und das ist der Sinn der Sache. Die Standardantwort lautet "nicht laden", und jedes "Ja" muss sich seinen Platz verdienen.
Die Zahlen
Über die gesamte Arbeit hinweg zog die Embed- und Skript-Umstellung weit über zwei Sekunden blockierender Arbeit aus dem initialen Laden eines schweren Artikels. Die Total Blocking Time auf der Testseite fiel in den grünen Bereich, bequem unter die 200-ms-Marke, und die Überschrift begann ungefähr in der Zeit zu rendern, die der Server immer schon versprochen hatte. Der Lighthouse-Performance-Score stieg von den hohen Siebzigern in die niedrigen Neunziger, aber der Score war nie der Punkt. Der Punkt war, dass die Seite aufhörte, gegen die Leserin zu kämpfen.
Was es dauerhaft machte, war, jede Änderung auf einem gedrosselten Mobilprofil zu messen, nicht auf einem Entwicklerlaptop. Die blockierende Zeit, die das Erlebnis ruiniert, ist auf schneller Hardware nahezu unsichtbar, und genau deshalb überlebt sie in Produktion so lange.
Erkenntnisse
- Behandle jedes Drittanbieter-Skript als schuldig, bis seine Notwendigkeit bewiesen ist. Die Standardeinstellung sollte "nicht laden" sein, nicht "laden und hoffen".
- Ordne zu, bevor du handelst. Nutze das Flammendiagramm des Hauptthreads und die web-vitals-Attribution, um das Skript zu finden, das dich wirklich kostet, nicht das, von dem du es annimmst.
- Schiebe nach Sichtbarkeit mit
IntersectionObserverauf und nach Absicht mit Fassaden. Die meisten Leser lösen den teuren Pfad nie aus, und genau das willst du. - Greif zu
fetchpriority="low"undrequestIdleCallback, um aufgeschobene Arbeit aus dem kritischen Fenster herauszuhalten. Einzeilige Änderungen, übergroße Wirkung. - Neutralisiere auf dem Server, nicht im Client. Sobald ein
<iframe src>den Browser erreicht, hast du die Anfrage bereits verloren. Und denk daran, dass React ein<script>, das du als String injizierst, nicht ausführt. - Achte auf das Produkt, nicht nur auf die Kennzahlen. Eine Lösung, die einen Videotitel verbirgt oder ein Embed kaputt macht, ist eine Regression mit schönem Score. Interaktionsgesteuertes Laden ist, wie du beides bekommst.
- Miss auf einem gedrosselten Handyprofil. Die blockierende Zeit, die das Erlebnis ruiniert, ist auf schneller Hardware unsichtbar.
Die Überschrift stand die ganze Zeit im HTML. Die eigentliche Arbeit bestand darin, dem Browser beizubringen, sie zu rendern, bevor er die Besorgungen aller anderen erledigt.

Verfasst von
Miracle Kalu
Senior Full Stack Engineer
Hat dir das Gefallen?
Ich bin verfügbar für Senior-Engineering-Rollen und technische Beratung. Lass uns reden.
Kontakt aufnehmen →Veröffentlicht 14. Juni 2026 · 11 Min. Lesezeit