Leute, heute rede ich nicht über Unsinn, sondern über eine große Falle, in die ich in den letzten zwei Tagen getappt bin, und wie ich da wieder herausgekommen bin. Die Sache ist so: Wir haben online ein altes Projekt, das immer gut lief. Doch neulich meldeten Nutzer gelegentlich weiße Bildschirme. Die Fehlerquote war zwar nicht hoch, aber bei der großen Nutzerbasis kamen trotzdem täglich haufenweise Error-Logs in mein Postfach. Zuerst dachte ich, das Backend hätte Aussetzer, aber nach langem Suchen war dort alles in Ordnung. Dann schaute ich mir das Frontend-Monitoring an und war direkt sprachlos – es waren alles JS-Fehler, und sie konzentrierten sich alle in der komprimierten Bundle-Datei.
Ihr wisst ja, dass JS in der Produktion komprimiert ist – eine Zeile mit Zehntausenden Zeichen, die Fehlermeldungen sind kaum lesbar. Ich starrte auf dieses „Unexpected token“ und hatte das Gefühl, es würde mich auslachen. Später aktivierte ich Sourcemaps zur Lokalisierung und stellte fest, dass die Fehler an den unterschiedlichsten Stellen auftraten, aber alle eines gemeinsam hatten: Sie standen in Bedingungen oder nutzten neue ES6+-Syntax.
Da wurde mir klar, dass das Problem wahrscheinlich an der Komprimierungskonfiguration lag. Unser Projekt verwendet ein älteres Komprimierungstool, und damals haben wir aus Bequemlichkeit die Standardkonfiguration genutzt, ohne sie genauer anzusehen. In der Standardkonfiguration gibt es einige Optimierungen wie „drop_debugger“ oder die Spielereien in „compress“, die für alten Code eine Katastrophe sind.
Ich gebe euch das typischste Beispiel: Das Komprimierungstool löscht standardmäßig Bedingungen, die es für „immer falsch“ hält. Zum Beispiel schreibt ihr im Code if (typeof window !== 'undefined'), um SSR zu unterstützen oder Fehler in bestimmten Umgebungen zu vermeiden. Aber der Komprimierer hält diese Bedingung für überflüssig, weil er aus dem Kontext schließt, dass window sicher existiert, und löscht den gesamten if-Block. Die Folge: Auf echten Geräten, in bestimmten WebView-Umgebungen, sind einige Eigenschaften von window nicht zugänglich, und der Code stürzt direkt ab.
Noch ärgerlicher ist, dass der Komprimierer Variablennamen in Funktionen extrem verkürzt, z.B. userName zu a und orderList zu b. Das ist meistens kein Problem, aber wenn ihr im Code eval oder new Function verwendet, die externe Variablennamen referenzieren, stimmen die Namen nach der Komprimierung nicht mehr überein, und es gibt direkt einen ReferenceError. Wir haben online einige alte Module, die das nutzen – normalerweise kein Problem, aber nach der Komprimierung sind sie kaputt.
Deshalb habe ich mich in den letzten zwei Tagen zusammengerissen und einen Nachmittag damit verbracht, diese Komprimierungskonfiguration zu untersuchen. Nach der Änderung – ratet mal? Die Online-Fehler sind um 80 % gesunken! Das ist kein Hokuspokus, sondern einfach die richtige Konfiguration.
Konkret habe ich mehrere Stellen geändert. Ihr könnt euch das notieren, vielleicht rettet es euch eines Tages:
Erstens: In den compress-Optionen habe ich conditionals und dead_code deaktiviert. Das verhindert, dass der Komprimierer schlau sein will und Codezweige löscht. Dadurch sinkt die Komprimierungsrate etwas, vielleicht ein paar KB mehr, aber dafür bekommt ihr Stabilität – das ist es wert! Besonders für alte Projekte mit vielen Kompatibilitätsprüfungen sind diese beiden Optionen Zeitbomben.
Zweitens: In mangle habe ich die eval-Option auf true gesetzt. Das bedeutet: Wenn im Code eval oder new Function erkannt wird, werden die Variablennamen darin nicht umgeschrieben oder die Variablen in diesen Funktionen nicht gekürzt. Das reduziert zwar die Komprimierungseffizienz, ist aber besser als ein Absturz in der Produktion.
Drittens: Ich habe auch unused in compress deaktiviert. Diese Option löscht Parameter, die „definiert, aber nicht verwendet“ sind. Manchmal lassen wir aber absichtlich einen Parameter als Platzhalter, z.B. in Callback-Funktionen, wo erst der dritte Parameter nützlich ist und die ersten beiden zur festen Signatur gehören. Der Komprimierer weiß das nicht, sieht, dass die ersten beiden nicht verwendet werden, und löscht sie direkt. Dadurch verschieben sich die Callback-Parameter, und beim Aufruf erhält man undefined, die Logik gerät durcheinander.
Viertens, und das wird am leichtesten übersehen: die ascii_only-Einstellung in output. Wenn euer Code chinesische Zeichen oder Emojis enthält, empfehle ich, diese auf false zu setzen, sonst werden sie alle in Escape-Sequenzen wie \uXXXX umgewandelt. Die Funktion bleibt gleich, aber die String-Länge explodiert, und einige alte Browser haben Probleme mit sehr langen Strings, was zu Hängern oder Fehlern führen kann.
Jedenfalls ist meine aktuelle Konfiguration der Modus „Kümmere dich nicht um unnötiges“. Der Zweck der Komprimierung ist es, die Größe zu reduzieren, aber wenn man dabei die Funktionen kaputt macht, ist der gesparte Traffic die Überstunden nicht wert. Ich habe nachgerechnet: Nach der Konfigurationsänderung ist die Bundle-Größe nur um etwa 15 % gestiegen, aber die Online-Fehlerrate ist von mehreren hundert pro Tag auf einstellige Zahlen gesunken. Diese paar KB sind doch egal.
Zum Schluss eine Warnung: Nach der Änderung der Komprimierungskonfiguration müsst ihr unbedingt einen vollständigen Regressionstest laufen lassen, besonders für Stellen mit Lazy Loading von Routen oder dynamischem import. Fragt nicht, woher ich das weiß – ich habe viel Lehrgeld gezahlt. Jedenfalls läuft es jetzt stabil, und ich kann wieder gut schlafen. Wenn ihr ähnliche Probleme habt, zweifelt nicht an der Logik, sondern schaut zuerst, ob eure Komprimierungskonfiguration nicht eher schadet als nützt.