Stranger Than Usual

Give me the microplastics! They belong in the ocean!

Worst Premade Ever

Billige Zipbombe

Eine Zipbombe (oder Archivbombe) ist eine komprimierte Datei, die zu einer extrem großen Datei entpackt wird. Normalerweise erstellt man Zipbomben direkt durch genaue Kenntnis des Kompressionsverfahrens ohne tatsächlich Daten zu komprimieren. So kann man zum Beispiel eine 42 kiB große Datei auf 4,5 Petabyte aufblähen.

Die billigere Variante ist, einfach sehr gut zu komprimierende Daten zu nehmen und sie zu komprimieren. Deutlich weniger elegant und 4,5 Petabyte wird man damit nicht hinkriegen. Dennoch ganz lustig. Ein Nutzen für eine solche Datei ist zum Beispiel, Scraper, die sich nicht an Regeln halten (z.B. and die robots.txt) halten, abzustrafen.

Meine eigene billige Zipbombe

Cool, so etwas will ich auch haben. Ich habe aber nicht genug Kenntnis über Kompressionsverfahren um so etwas zu basteln, möchte keine vorgefertigte Zipbombe nehmen (deren Hash könnte ja auf einer ignore-list stehen) und hätte gerne eine Zipbombe, in der gültiges HTML steckt. Womit auch erklärt wäre, warum ich neulich eine große HTML-Datei möglichst schnell nach stdout schreiben wollte (dazu gibt es übrigens unten noch ein Update).

Ich habe drei verschiedene Kompressionsverfahren ausprobiert, die alle im HTTP-content-encoding header erlaubt sind: Brotli, gz und Zstandard. Ich habe jeweils die höchste Kompressionsrate gewählt (für gz allerdings nicht Zopfli, das wäre mir dann doch zu langsam gewesen, also nur herkömmliches gzip). Ich habe dann wie beschrieben eine etwa 42 GiB große Datei erzeugt und diese on-the-fly zur Kompression gegeben.

Die Messungen sind nur mit time gemacht, könnten also Ausreißer enthalten. genbig ist hier das Rust-Programm, das die HTML-Daten erzeugt. Das sind die Ergebnisse:

gzip

$ time  genbig | gzip --best --stdout > big.html.gz
real    1m15,029s
user    1m12,729s
sys 0m14,630s

Dauert eine Weile, und die Ergebnisdatei ist etwa 126 MiB groß. Eigentlich würde ich gz gerne mit reinnehmen, weil es am weitesten verbreitet ist (viele Bots unterstützen meiner Erfahrung nach überhaupt keine Transportkompression, ganz zu Schweigen von Brotli), aber 126 Megabyte sind mir zu groß, da werden auf meiner Seite zu viele Resourcen genutzt.

Brotli

$ genbig | brotli --force --best -o big.html.br
real    32m47,275s
user    32m36,105s
sys 0m16,348s

Das hat eine gute halbe Stunde gebraucht, also eine Größenordnung mehr als gzip. Autsch! Auf der positiven Seite: Die Datei ist angenehme 35 kiB groß. Damit kann ich arbeiten.

Zstandard

Und der Neueste im Bunde. Um hier die Performance vergleichen zu können, habe ich es einmal mit einem Kompressions-thread und einmal mit vier Kompressions-Threads laufen lassen. Es gab dabei jeweils immer noch einen separaten I/O-Thread. Zuerst die Version mit einem Thread:

$ time genbig | zstd -19 -T1 --no-content-size --size-hint=43009MiB -o big_single.html.zst
real    0m53,649s
user    1m4,932s
sys 0m19,570s

Dann die Version mit vier:

$ time genbig | zstd -19 -T4 --no-content-size --size-hint=43009MiB -o big.html.zst
real    0m20,540s
user    1m27,782s
sys 0m19,051s

Von der Zeit her also beides deutlich besser als gzip, ganz zu schweigen von Brotli. Das Ergebnis ist für beide Varianten gleich und etwa 3,7 MiB groß. Das kommt schon in eine brauchbare Richtung, ist aber Größenordnungen hinter der Brotli-Variante.

Fazit

Es war ganz lustig, das mal ausprobiert zu haben. Bisher habe ich diese Dateien noch nirgendwo im Einsatz, weil ich noch nicht genau weiß, wie ich sie einsetzen will. Ich möchte keine Suchmaschinencrawler abschrecken, und erst recht nicht, dass ein regulärer User aus Versehen auf diese Dateien stößt. Eher will ich LLM-Scraper bestrafen, die sich nicht an die robots.txt halten (die sie momentan aber noch nicht aus diesem Blog aussperrt) oder die ganzen Scanner, die bei mir nach Lücken suchen. Letztere können aber auch gutmütig sein, aber davon würde ich nicht unbedingt ausgehen.

Viele dieser Bots unterstützen wie gesagt sowieso keine Transportkompression, und die meisten, die es tun, kein Brotli. Und speziell die gzip-Datei ist mir zu groß für den Spaß. Trotzdem: War mal ganz lustig, das auszuprobieren.

Bonus: I/O-Optimierung

Zstandard ist wirklich schnell, besonders mit mehreren Threads. Tatsächlich war es so schnell, dass ich dafür ein Rust-Programm optimieren musste, so dass es schneller nach stdout schreibt. Dummerweise war das Rust-Programm danach zwar deutlich schneller als die Python-Variante, aber immer noch zu langsam. Mit vier Threads:

real    0m51,731s
user    2m8,343s
sys 0m22,070s

Wenn man sich den Prozess während der Laufzeit anschaut, stellt man fest, dass genbig 100 % eines Kerns auslastet, und zstd kommt gerade mal auf 190 % (von den mit 4 + 1 Threads möglichen 500 %). Das verfälscht natürlich die Messung. Außerdem: Verdammt soll ich sein wenn ich kein Programm schreiben kann, das nichts tut außer immer wieder das gleiche nach stdout zu schreiben und dabei schneller ist als ein Programm, dass dieselben Daten von stdin liest und komprimiert. Das wäre peinlich.

Aber ich kann ja sicher etwas verbessern. Zum Beispiel werden sehr viele kleine Schreiboperationen durchgeführt. Die gehen zwar in einen Buffer, trotzdem muss nach jeder Operation auf Fehler geprüft werden. Wenn man das eine Milliarde mal macht, kommt da richtig Rechenzeit zusammen. Also habe ich mir gedacht, ich schreibe einfach mehrere Einheiten von dem sich wiederholenden Teil in einem write-Aufruf:

fn gen_body_part() -> String {
    // 8 kiB is the default buffer size of BufWriter
    repeat(REPEATED).take((1024 * 8 ) / REPEATED.len() + 1).collect()
}

fn main() -> Result<()> {
    let body_part = gen_body_part();
    let body_bytes = body_part.as_bytes();

    let mut out = BufWriter::new(stdout().lock());

    writeln!(out, "{}", PREFIX)?;

    let repeats = APPROX_SIZE / body_bytes.len();
    for _ in 0..repeats {
        out.write_all(body_bytes)?;
    }

    writeln!(out, "{}", SUFFIX)
}

Das Ergebnis ist besser, aber nicht wesentlich besser:

real    0m48,688s
user    2m4,117s
sys 0m20,982s

genbig ist immer noch bei 100 % Auslastung, zstd bei etwa 200 %. Aber ich habe ja auch einen Fehler gemacht: Ich habe das, was ich auf einmal schreibe, ein sehr kleines bisschen größer gemacht als den Buffer. Wenn ich also die Funktion oben ändere in

fn gen_body_part() -> String {
    // 8 kiB is the default buffer size of BufWriter
    repeat_n(REPEATED, (1024 * 8) / REPEATED.len()).collect()
}

Dann hat genbig zwischendurch endlich Zeit Luft zu holen, zstd rechnet auf allen Threads am Limit und die Laufzeit verringert sich auf… Trommelwirbel

real    0m20,540s
user    1m27,782s
sys 0m19,051s

Etwa 20 Sekunden. Eine deutliche Verbesserung.

Ich hätte natürlich auf die Buffergröße anpassen können. Vermutlich gibt es noch einen Haufen Optimierungsmöglichkeiten hier. Und das hier ist ein sehr kurzes Programm. Ich frage mich, wie viel Software ich geschrieben habe, die langsam ist, weil sie an irgendeinem dummen Flaschenhals feststeckt. Ich frage mich auch, wie viel Software da draußen viel langsamer ist, als sie es sein müsste, weil irgendjemand einen dummen Flaschenhals übersehen hat. Naja, jedenfalls habe ich wieder etwas gelernt.

Digital Independence Towel Day

Frohen Towel Day an euch alle! Wisst ihr alle, wo euer Handtuch ist?

Heute Abend habe ich mich online mit ein paar Freunden zusammengesetzt, um zusammen auf Alternativen zu großen Techkonzernen umzusteigen. So à la Digital Independence Day, aber halt nicht am ersten Sonntag des Monats wo ich diesen Monat weder Zeit noch Ideen hatte, was ich tun sollte.

Heute haben wir aber ein paar Sachen geschafft:

  • Freundin A hat auf ihrem Smartphone eine googlefreie Android-Variante installiert und zum ersten Mal auch den Signal Messenger
  • einem linuxinteressierten Freund B haben wir Informationen zu einem möglichen Umstieg/Teilumstieg auf Linux gegeben. Das war eine gute Arbeitsteilung: Ich, erfahrener Linuxnutzer, was möglich ist und wie man bestimmte Sachen macht. A konnte ihm Erfahrungswerte auch nichttechnischer Sicht geben (sie ist mit Linux Mint sehr gut klar gekommen). Freundin C konnte Hilfe bei der Installation anbieten (was für mich schwierig wäre, weil alle anderen drei in Hamburg wohnen und ich nicht).
  • C hat mit dem Umstieg auf Posteo angefangen. Sie hatte schon einen Account, aber bisher noch keine Zeit gehabt, umzuziehen.
  • Ich habe das alte Linux 19.10 auf meinem Spielerechner (auf dem außerdem noch Windows 10 läuft) mit Linux 26.04 überbügelt und erste Erfahrungen gemacht, wie gut Valves Wine-Fork Proton funktioniert. Erste Erfahrungen waren positiv. Als nächstes probiere ich anspruchsvollere Spiele. Dann kann ich auch B erzählen, was er dort erwarten kann.

Alles in Allem: Ein erfolgreicher Abend in angenehmer Atmosphäre.

PS: Wir haben das Ganze ironischerweise über Discord organisiert und durchgeführt. Das zu ändern steht auch noch auf der Agenda.

Rollenspielszenen: Morgenlied

Rollenspielszene: Die Gruppe hat beschlossen, am nächsten Morgen früh aufzubrechen. Fredegar Stolzfuß, Barde, ist als erster wach. Um den anderen Gruppenmitgliedern ein schönes Aufwachen zu ermöglichen, weckt er sie mit einem fröhlichen Morgenlied.

Trotz seiner passablen Menschenkenntnis rechnete er nicht damit, dass das Thivuc überhaupt nicht gefallen könnte. Ein gezielter Wurf mit einem Stiefel, und Fredegar bricht das Lied ab. Banausen!

To flush() or not to flush()

Ich habe heute mal ein bisschen herumgespielt, für dass ich eine große HTML-Datei brauchte, die möglichst einheitlich aussehen sollte. Also wirklich groß. Im Gigabytebereich. Diese Datei sollte dann auch nicht in eine Datei geschrieben, sondern direkt an ei verarbeitende Programm weitergegeben werden. Wozu? Dazu vielleicht später mal mehr.

Also habe ich mir ein sehr einfaches Python-Script geschrieben, das zuerst den Beginn des HTML-Dokuments nach stdout schreibt, zuletzt das Ende und dazwischen in einer Schleife einfach immer wieder dieselbe Zeile:

for i in range(approx_size // len(repeated_element)):
    print(repeated_element)

approx_size ist hier die ungefähre Zielgröße der Datei (eigentlich 42 GiB).

Mir ist dann aufgefallen, das je nachdem an welches Programm die Daten weitergereicht werden, das Zielprogramm die Daten schneller verarbeitet als Python sie schreiben kann. Also habe ich das gemacht, was jeder Rust-Enthusiast machen würde: Das ganze noch einmal in Rust geschrieben. Die Ausgabedaten zwischen den beiden Versionen sollten identisch sein. Das Rust-Programm ist auch sehr kurz (ein paar Konstanten mit dem HTML-Inhalt und der Anzahl der Wiederholungen habe ich vorher definiert).

fn main() -> Result<()> {
    let mut out = stdout().lock();

    writeln!(out, "{}", PREFIX)?;

    for _ in 0..REPEATS {
        writeln!(out, "{}", REPEATED)?;
    }

    writeln!(out, "{}", SUFFIX)
}

Also habe ich es ausgeführt und… es lief deutlich langsamer als die Python-Version. Ich habe das mal mit ≈ 42 MiB großen Dateien getestet, weil das deutlich schneller ging:

$ hyperfine "./genbig.py > /dev/null"
Benchmark 1: ./genbig.py > /dev/null
  Time (mean ± σ):     337.7 ms ±  15.5 ms    [User: 333.5 ms, System: 4.3 ms]
  Range (min … max):   324.1 ms … 373.2 ms    10 runs
$ hyperfine "target/release/genbig > /dev/null"
Benchmark 1: target/release/genbig > /dev/null
  Time (mean ± σ):     780.3 ms ±   9.1 ms    [User: 457.9 ms, System: 321.9 ms]
  Range (min … max):   773.5 ms … 805.2 ms    10 runs

Wtf? Das ist weniger als halb so schnell! Woran liegt das? Es ist der release-build, also liegt es sicher nicht an mangelnder Compileroptimierung. Ich habe auch den stdout-lock nur einmal ganz am Anfang geholt, damit das Programm keine Hindernisse zum Schreiben hat.

Ich bin ziemlich schnell auf den Gedanken gekommen, dass es am flushen liegen könnte. print() in python verhält sich nämlich unterschiedlich, je nachdem ob man auf eine Konsole schreibt oder irgendwoanders hin. Bei einer Konsole wird nach jedem print() ein flush gemacht (oder zumindest nach jedem Zeilenumbruch, ich habe das nicht genau nachgeschlagen). Mir ist das wieder eingefallen, weil in umgekehrt mal Probleme damit hatte, dass Python nicht geflushed hat, wenn ich es wollte.

In rust wiederum ist es bei stdout fix. In der Referenz zu stdout steht:

By default, the handle is line-buffered when connected to a terminal, meaning it flushes automatically when a newline (\n) is encountered. For immediate output, you can manually call the flush method.

Aha. Während Python also alles schön in einen Buffer schreibt und dann flushed, wird bei Rust in jeder einzelnen Zeile geflushed. Zum Vergleich: Wenn stdout die Konsole ist, dann sieht es schon ganz anders aus. Dann sind es bei Rust etwa 3 Sekunden und bei Python etwa 4,5 Sekunden (die Konsole ist sehr lahm).

Das Problem ist einfach gelöst: es gibt std::io::BufWriter. Damit wird irgendein Writer gebuffert und nur noch geflushed, wenn der Buffer voll ist oder man manuell flush() aufruft. Ich musste stdout also nur wrappen:

let mut out = BufWriter::new(stdout().lock());

und am Ende noch einmal flush() aufrufen (das passiert auch automatisch, aber dann werden möglicherweise auftretende Fehler ignoriert), und schon geht es deutlich schneller:

$ hyperfine "target/release/genbig > /dev/null"
Benchmark 1: target/release/genbig > /dev/null
  Time (mean ± σ):      26.5 ms ±   1.2 ms    [User: 25.8 ms, System: 1.1 ms]
  Range (min … max):    25.3 ms …  30.7 ms    93 runs

Cool. Also von weniger als halb so schnell zu mehr als zehn Mal so schnell. Es gibt noch ein paar andere Stellen, an denen ich Optimierungsmöglichkeiten sehe, aber das ist mir erst einmal schnell genug, schneller kann es die andere Seite eh nicht verarbeiten.

Fazit

Was lernen wir daraus? Einfach nur Rust zu verwenden löst keine Performanceprobleme, wenn man Flaschenhälse an völlig anderen Stellen hat. Gerade bei I/O sollte man ein bisschen hinter die Abstraktion schauen und dementsprechend optimieren. Zumindest, wenn man Optimierung braucht.

Genereller betrachtet trifft das auch auf andere Performanceprobleme zu. Zum Beispiel wenn man einen Algorithmus, der eigentlich in linearer Zeit laufen könnte, aus versehen quadratisch macht. Da hilft dann auch keine schnellere Programmiersprache.

Rollenspielszenen: endlich feiern

Rollenspielszene. Wir haben erfolgreich Amulette erbeutet, die uns vor der wilden Jagd verbergen. Zwei Wochen später, nach langen Märschen durch die Marschen und Goblingebiete sind wir endlich wieder in der Zivilisation angekommen, in Flussauge, der Hauptstadt des Königreichs Havaria. Nach vielen Monaten das erste Mal in der Zivilisation ohne von der wilden Jagd verfolgt zu werden oder von einem durchgeknallten Magier die Stadt gegen sich aufzubringen. Das bedeuter Feiern!

Mavas hat jedoch andere Prioritäten. Erst muss er der elfischen Botschaft hier einen Besuch abstatten. Er weiß, dass Therith feiern möchte und will ihr anbieten, ohne sie zur Botschaft zu gehen er dreht sich um und…

Therith steht schon mit irgendetwas knusprig Gebratenem in der Hand neben ihm, das sie sich gerade gekauft hat.

Zstandard

Wer dieses Blog schon länger mitverfolgt weiß, dass ich einen gewissen… Fetisch für Datenkompression habe. Meine Bilddateien optimiere ich, soweit das verlustfrei geht. Die über HTTP ausgelieferten Inhalte dieses Blogs werden schon seit Jahren als Zopfli-optimiertes gz ausgeliefert, seit zwei Jahren auch Brotli-komprimiert (letzteres hat nur mangels Unterstützung der nginx-Version in Debian so lange gedauert).

Deswegen war ich gestern ein bisschen überrascht, dass es schon seit zwei Jahren einen weiteren Kandidaten auf dem Markt für HTTP-Content-Encoding gibt: Zstandard. Gibt es schon seit dem vergangenen Jahrzehnt und seit 2024 auch bei Chromium-basierten Browsern und Firefox als Content-Encoding. Wie konnte mir das entgehen? Und was kann diese „neue“ Kompression?

Glücklicherweise habe ich unter anderem diesen Blogpost über den Vergleich von ZStandard mit anderen Verfahren gefunden. Der Autor hat einen Kompressions-Tester für Websites geschrieben und analysiert, wie ZStandard im Verhätlnis zu Klartext, gzip und Brotli abschneidet.

Long story short: ZStandard ist sehr gut dabei, wenn es um Geschwindigkeit geht, bleibt aber bei höheren Kompressionsraten weit hinter Brotli zurück. Mit anderen Worten: Wenn man, so wie ich für dieses Blog, die Dateien nicht on-the-fly sondern im Voraus kompromiert (und sich dementsprechend ein bisschen mehr Zeit leisten kann), dann ist Brotli nach wie vor die beste Wahl. ZStandard glänzt hingegen dort, wo es auf Kompressionsgeschwindigkeit ankommt, z.B. bei dynamischen Inhalten. Für dieses Blog nicht relevant, aber ich behalte das mal im Hinterkopf.

Goodbye Search Console

Dieses Blog ist ja ein reines Hobbyprojekt. Der Kreis der Leser_innen dieses Blogs vermutlich sehr klein. Ich habe nie versucht, Geld damit zu verdienen. Trotzdem streichelt es meine Eitelkeit ein wenig, wenn ich mitbekomme, dass jemand mein Blog liest. Das war einer der Hauptgründe, warum ich irgendwann (vermutlich Ende 2020 / Anfang 2021 angefangen habe, Google Search Console zu verwenden um zu schauen, welche Seiten meines Blogs indziert sind (d.h. welche man theoretisch über Google finden kann) und welche meiner Blogposts tatsächlich von anderen über Google gefunden werden.

Besonders ersteres war immer interessant, denn mein Blog hat keine eigene Suchfunktion. Nur leider hat Google nie alle meine Seiten indiziert. Und Google ist sehr intransparent darüber, warum etwas nicht indiziert wird. Das machen sie, weil sie nicht wollen, dass SEO-Leute (Search Engine Optimization) Lücken im System nutzen um ihre Websites besser heraus zu bringen. Google will (oder wollte, ob das immer noch so ist, ist eine Frage, die ich hier nicht diskutieren will) Seiten anzeigen, die den Suchenden nützen, nicht den Gesuchten.

Wenn ich versucht habe, von Drittparteien Informationen zu bekommen, bin ich meist auf Seiten gelandet, die einen sehr starken Fokus aufs Geschäftliche haben und ein Blog meist nur als Produkt betrachten, das es zu vermarkten gilt. Das will ich aber nicht. Ich will nicht den Markt entscheiden lassen, über welche Dinge ich schreiben muss. Ich schreibe, was ich will, und das ist ein bunter Themenmix. Die andere Hauptkategorie, wie Google wohl Seiten bewertet ist, die Links auf eine Seite zu zählen. Die Idee: Je mehr Links, desto relevanter ist die Seite. Auch hier habe ich nicht viel vorzuweisen.

Also dümpelte meine Seitenindizierung so vor sich hin, und es war immer ein bisschen wie ein süchtig machendes Glücksspiel: Das nächste Mal, wenn ich mich einlogge, ist die Anzahl der Seiten vielleicht höher! Wenn ja: Endorphine. Wenn nein: Neues Spiel, neues Glück, ich schaue nächste Woche wieder vorbei. Ob das so gut für meine mentale Gesundheit ist? Vermutlich nicht.

Vor etwa einem Jahr ist dann die Indizierung stark zusammengebrochen, hat sich aber nach und nach erholt und schwankte stark. Vor etwa einem Monat ist sie aber extrem eingebrochen und von über 400 indizierten Seiten über wenige Wochen auf 0 (Null. Keine einzige) Seite runtergegangen. Warum? Keine Ahnung. Herausfinden kann ich es nicht. Ich weiß nur, dass selbst die (auf Google) erfolgreichsten Posts (nämlich der über aircrack-ng auf einem Raspberry Pi 5, der über Stempel aus Photopolymer und der über den Datenelch auf dem 36C3 auch alle deindiziert waren.

Was tun?

Zunächst einmal hat mich das ein bisschen geärgert. Dann habe ich es aber als Chance begriffen: Warum soll ich mich überhaupt darum kümmern, was Google mit meiner Seite macht? Ich habe viele Argumente dagegen

  • Ich nutze Google als Suchmaschine selbst nur noch in Notfällen, fast imemr nutze ich DuckDuckGo.
  • Google (oder eher: Alphabet, der Mutterkonzern) hat es nie groß mit dem Datenschutz gehabt. Scheiß auf Google!
  • Nachzusehen, wie die Indizierungsstatistiken sind, glich bei mir eher dem zwanghaften Verhalten eines Spielsüchtigen. Nein danke.
  • Um Google gegenüber sicherzustellen, dass die Domain tatsächlich mir gehört, musste ich in einen TXT-Eintrag (google-site-verification=[…]) in die DNS-Daten eintragen. Jeder konnte also sehen, dass ich Googles Bitch war.

Also habe ich jetzt die DNS-Einträge gelöscht und gehe auf harten Entzug. Auf DuckDuckGo ist mein Blog übrigens noch gut indiziert und neue Posts werden auch schnell hinzugefügt. Außerdem habe ich mich auf ein paar Bloglisten eintragen lassen, zum Beispiel auf Read Great Blogs. Dort werden dann über mein Atom-Feed auch aktuelle Blogposts gelistet.

Ich würde mich natürlich trotzdem immer freuen, wenn jemand mein Blog verlinkt, auch wenn ich davon wahrscheinlich so ohne Weiteres nichts mitkriegen würde. Umgekehrt habe ich dieses Jahr ja selber das Bloglinkprojekt laufen, in dessen Rahmen ich jede Woche auf einen Post eines (hoffentlich jedes Mal anderen Blogs) verlinke. Es gibt eine Menge WWW außerhalb der großen Plattformen. Es ist Zeit, das Netz zurückzuerobern!

Webbloat reduzieren

In den letzten Wochen habe ich beruflich an einer Angular-App gearbeitet. Der Kunde nutzt überall Angular, und möchte das auch hier nutzen. Der Kunde hat auch schon eine Bibliothek mit ordentlichen Komponenten, passend zum Corporate Design. Das Projekt ist für mich insofern ungewöhnlich, dass ich sonst nie reine Frontendarbeit mache, sondern sonst immer Backend und Frontend. Aber das ist hier irrelevant.

Wir haben die Entwicklung also vor knapp vier Wochen gestartet. Vorher war die App leer. Die App macht bisher auch nicht viel, der Kunde will erst einmal einen Klickdummy für eine Schulung, und die Backend-Endpunkte (wie gesagt, ein anderes Team) sind sowieso noch nicht fertig. Bzw. es wurde noch nicht damit angefangen (ja, ich weiß, das wird höchstwahrscheinlich noch… interessant).

Diese Woche habe ich mal darauf geschaut, wie groß das Javascript unserer App ist. Fuck! 1,5 MiB. Dabei macht die App praktisch noch nichts. Keine komplizierten, ungewöhnlichen Protokolle in den Abhängigkeiten, wie z.B. bei der Winkekatzenseite der Fall ist. Nur Standard-Angular und die Komponentenbibliothek, die der Kunde bereitstellt (von der nur ein Bruchteil tatsächlich genutzt wird).

So schnell geht es also, das Javascript (und CSS) unnötig aufzublähen. Seit ich auf so etwas achte (auch bei Websites, an denen ich nicht selber arbeite, ich bin das schon fast so obsessiv wie mit Datenkompression) habe ich mich immer gefragt, wieso so viele Seiten so groß sind, obwohl ihr Content das bei weitem nicht rechtfertigt. Meine aktuelle Hypothese: Es ist zu einfach. Man nimmt ein bisschen Bequemlichkeit (ein JS-Framework hier, eine Komponentenbibliothek da) und – schwupps! Alles ist riesig, und der größte Teil davon wird nicht gebraucht. Und niemand bezahlt einen dafür, das JS deutlich kleiner zu machen. In der Regel geht es um zwei Dinge: Features, und wieviel Zeit (und damit Geld) es kostet, sie zu implementieren.

Ich habe trotzdem einen gewissen Stolz, und möchte dieses JS deutlich kleiner kriegen. Ich weiß noch nicht genau wie, aber es muss gehen. Privat kriege ich das ja auch hin (meist, weil ich überhaupt kein Javascript und keine CSS-Frameworks verwende). Und ich möchte hier ein Beispiel geben, wie man das in einem einfachen Fall machen kann. Doch zuerst ein kleiner Exkurs:

Exkurs: Ein Känguru-Running-Gag.

Ein running Gag in den Känguru-Büchern von Mark-Uwe Kling ist, dass sich das Känguru (und später Mark-Uwe Kling selbst) sich über Preise beschweren, indem sie einen Satz in der Form „N Euro? Das sind ja fast 2N D-Mark! 4N Ostmark! 20N Ostmark auf dem Schwarzmarkt“ von sich geben. Zum Beispiel:

20 Euro? Das sind ja fast 40 D-Mark! 80 Ostmark! 400 Ostmark auf dem Schwarzmarkt!

Irgendwann dieses Jahr wollte ich diesen Spruch auf Mastodon in einem Kommentar bringen, wusste aber nicht mehr, wie das Verhältnis von D-Mark zu Ostmark zu Ostmark auf dem Schwarzmarkt war. Also habe ich eine Internetsuche gemacht und bin unter anderem auf den Känguru-Rechner gestoßen. Ein kleine HTML-Seite, die nicht viel macht, außer bei der Eingabe einer der vier Werte die anderen automatisch auszurechnen. Der Link zu dem Blogpost geht leider ins Leere, weil sich dir URL geändert hat (sollte nicht passieren), aber den Blogpost gibt es noch unter einere leicht anderen URL, tatsächlich hat sich hier nur ein „a“ in der URL in ein „ä“ umgewandelt.

Warum erwähne ich das? Zum Einen finde ich diese Idee von so einem Rechner ganz lustig. Das Internet braucht solche kleinen Späße. Dann kann man diesen Känguru-Satz wunderbar auf Dateigrößen umformen und dabei dieselbe Empörung zum Ausdruck bringen (z.B. „1,5 Megabyte? Das sind ja fast zwei Megabyte! Sechs Megabyte unkomprimiert! Zwanzig Megabyte im Debug-Build!“). Insbesondere aber ist diese Seite vom Inhalt her winzig, bringt aber trotzdem 232 kiB auf die Waage.

Zum Vergleich: Die Startseite meines Blogs ist, vor der Veröffentlichung dieses Posts hier, knapp 225 kiB groß. Knapp 158 kiB davon sind Bilder, knapp 70 kiB davon HTML (der größte Teil davon tatsächlich der eigentliche Textinhalt des Blogs), der Rest fällt auf das CSS.

Der Kängururechner ist also nicht riesig im Verhältnis zu manch einer aufgeblähten Seite, aber deutlich größer als er sein müsste. Und er ist vom Funktionsumfang her ziemlich klein, was ihn zu einem perfekten Demonstrationsobjekt macht, wie man seine Website kleiner kriegen kann.

Disclaimer: Ich will niemanden bloßstellen

Das ist vielleicht wichtig zu sagen, bevor es jemand falsch versteht: Ich will nicht den Autor dieses Rechners bloßstellen. Das ist jemand, der als Hobbyprojekt vor Jahren mal eben diesen Rechner gebaut und ihn seitdem vermutlich nie mehr angefasst hat. Der Autor fordert in dem Blogpost selbst dazu auf, den Code zuverbessern. Der Autor hat mittlerweile ein mit Hugo generiertes Blog, das deutlich weniger Platz einnimmt als der Känguru-Rechner. Der Autor hat explizit Wert darauf gelegt, dass dieses Blog nur noch Resourcen von der eigenen Domain lädt, um den Datenschutz zu verbessern.

Soweit ich das erkennen kann ist der Autor ein cooler Mensch. Ich zeige hier die Verbesserungsmöglichkeiten des Kängururechners nicht auf, weil ich sagen will, dass er schlechte Arbeit gemacht hat, sondern weil ich den Kängururechner lustig finde und er so klein ist, dass man ihn gut als Beispiel nehmen kann.

Die Verkleinerung

Mein erster Gedanke war: „Ich kriege das locker auf unter 10 kiB“. Der zweite Gedanke war: „Mist. Nicht wenn ich die Custom-Schriftart behalten will“. Also, wo fangen wir an?

HTML-Fehler

Der erste Schritt ist keine Verkleinerung. Es gibt ein paar Syntaxfehler im HTML. Die behebe ich zuerst, damit ich einen stabilen Stand habe auf dem ich arbeiten kann. Da ist zunächst ein <noscript>-Tag im <head> Da die Hauptfunktion dieser Seite (die Berechnung der Zahlen) nur mit Javascript läuft, ist ein <noscript>-Tag eine gute Idee. Dieses hier ist aber im <head> und enthält ein <p> und ein <img>.

Beim genaueren Draufschauen ist das aber nur ein Zählpixel eines Tracking-Scripts des Tracking-Systems piwik. Immerhin kein Google Analytics, und es liegt auf dem Server des Autor selber. Mein uBlock Origin hat das aber sowieso geblockt, also nehme ich direkt einmal den ganzen piwik-Teil raus. Stattdessen füge ich ein tatsächlich hilfreiches <noscript>-Tag hinzu.

Weg mit Bootstrap!

Das ändert leider noch nicht viel an der Größe. Laut den Browsertools jedoch sind bootstrap.min.css (121 kiB) und jquery.min.js (28 kiB) für den gößten Teil des Bloats verantwortlich. Also weg damit! Zudem wird das auch noch von Drittsystemen geladen, ist also potentiell ein Angriffspunkt und eine Tracking-Möglichkeit.

Die Sache hat nur einen Haken: Damit ist ein Großteil des Layouts weg. Aber das ist nichts, was man mit ein bisschen CSS nicht selber hinkriegt. Dank fle ist das mittlerweile ziemlich einfach. Mein Ergebnis sieht dem Original recht ähnlich. Ich könnte es noch ähnlicher kriegen, aber was mir gerade wichtig ist: Es sieht vom Format her so aus wie vorher und es ist responsive, d.h. es verhält sich auf verschiedenen Bildschirmgrößen vernünftig. Dafür musste ich auch noch eine media-query einbauen, aber das war kein großes Problem.

Oh Mist, es funktioniert nicht mehr!

Mit Bootstrap habe ich auch jquery entfernt. Dummerweise funktioniert damit das Berechnungsscript nicht mehr. Naja, kein Problem. Dank modernem Javascript braucht man jquery eigentlich nicht mehr. Ich kann zum das hier

var euro = $(".number#euro");

ohne jquery so schreiben:

const euro = document.getElementById("euro");

Ich bin mir ziemlich sicher, das ging auch schon vor jquery so. JQuery fügt allerdings noch ein paar Komfortfunktionen hinzu. Anstatt also wie mit jquery so auf ein Event zu lauschen:

euro.blur(function() {
  const value = parse(euro);
  changeNumber("euro", value);
});

mache ich es so:

euro.addEventListener("blur", () => {
  const value = parse(euro);
  changeNumber("euro", value);
});

Ach warte: Ich muss auch noch Stellen wie euro.text() in euro.innerText umwandeln. Danach geht aber alles super.

Schriftarten

Jetzt sind wir schon auf weniger als 24 kiB runter. Für das bisschen CSS und Javascript, das wir selber schreiben mussten, echt ordentlich. Das HTML, CSS und JS von uns ist jetzt unter 5 kiB groß. Den Rest der Größe macht hauptsächlich die verwendete Schriftart aus: Indie Flower. Die wird momentan automatisch von Googlefonts geladen. Der erste Nachteil daran ist natürlich Google. Aber die Schrift steht unter der Open Font License und wir können sie auch von unserem eigenen Server ausliefern. Mit etwa 20 kiB ist die sogar recht schlank.

Ich stelle hier aber die Frage: brauchen wir die Schrift? Gerade bei geschäftlichen Seiten ist die Antwort oft „Ja, das ist unsere Corporate Design“. In vielen Fällen kann man jedoch „Nein“ sagen. So wie ich in diesem Fall. Ich lasse die Referenz auf die Schrift aber mal drin, für den Fall, dass jemand sie installiert hat. Sonst wird als Fallback halt die erstbeste cursive-Schrift verwendet:

font-family: 'Indie Flower', cursive;

Finishing touches

Jetzt nur noch den Footer anpassen: den Link auf den ursprünglichen Autor korrigieren, mein Repo dazu verlinken… oh, warte, ich habe noch kein Repo. Und mit dem Repo brauche ich noch eine README-Datei. Alles kein Problem. Repo erstellt.

Es gibt noch kleine Verbesserungen, die ich machen würde. Allen voran ein Favicon. Aber das gehört nicht hier hin. Schaut euch das Repo an, vielleicht kommt da noch etwas.

Fazit

Der verkleinerte Kängururechner ist jetzt auf meinem Server. Sieht ein bisschen anders aus als das Original, insbesondere weil die Schriftart anders ist, funktioniert aber genau so gut. Und ist unter 5 kiB groß, mit Kompression sogar noch weniger.

War das hier jetzt ein Tutorial? Hell no. Ich habe fast nicht gezeigt, was ich gemacht habe und wie. In dem Oben verlinkten Repo kann man sich die Änderungen allerdings anschauen, die ersten paar commits sind die Änderungen, die ich in diesem Post erwähnt habe.

Hilft mir das jetzt, die 1,5 MiB von meinem Job zu verkleinern? Auch nicht, dort sind die Probleme etwas anders gelagert.

Trotzdem, es war ein schöner Weg für mich, mich zu entspannen

LMAA 🦙

Letzte Woche ist mir auf Mastodon „LMAA“ untergekommen. Nein, nicht was ihr denkt (oder was ich denke, was ihr denkt, leckt mich am Arsch). Sondern Lokale Märkte. Alternative Anbieter. Offiziell wird das „Lama“ ausgesprochen.

Die Seite ist eine durchsuchbare Liste von Online-Shops (oder typischerweise Offline-Shops, die auch Versand online anbieten). Wenn man also nicht bei Amazon, AliExpress oder einem anderen großen Moloch bestellen möchte und der Versand aus der Nähe erfolgen soll, kann man dort suchen. Man kann auch selber Shops vorschlagen. Nach eigenen Angaben werden alle Shops, die vorgeschlagen werden, überprüft, ob sie den Kriterien entsprechen.

Wenn ihr also etwas online bestellen möchtet: Schaut nach, ob es da etwas Passendes gibt. Vielleicht ist sogar ein passender Laden in eurer Nähe, und ihr könnt da auch so vorbei. Umgekehrt: Wenn ihr einen guten Onlineshop kennt (oder selber betreibt), der noch nicht gelistet ist und die Aufnahmekriterien erfüllt, schlagt ihn vor. Damit können den auch andere finden, und ihr unterstützt einen Shop, mit dem ihr selber schon gute Erfahrungen gemacht habt.

Logbuch: Netzpolitik hat in der letzten Folge auch auf diese Seite verwiesen, im Rahmen der Berichterstattung über den Digital Independence Day. Unter Gekicher beim Nennen des Namens, natürlich.

Bilder im Darkmode

Ich habe ja Ende Februar einen Darkmode für dieses Blog eingeführt. Weil ein paar Bilder hier aber transparente Teile haben und nur auf einem hellen Hintergrund funktionieren, habe ich für die Bilder grundsätzlich einen hellen Hintergrund eingestellt.

Das hat natürlich ein paar Nachteile. Insbesondere werden halt Bilder, die keinen hellen Hintergrund brauchen, trotzdem mit einem dargestellt. Das widerspricht der Idee des Darkmode.

Die meisten meiner Bilder hier haben sowieso keine transparenten Teile. Die Bilder mit transparenten Teilen brauchen nicht alle einen hellen Hintergrund. Also habe ich mich entschieden, den hellen Hintergrund optional zu machen. Bilder, die einen hellen Hintergrund benötigen, werden in den Metadatan speziell markiert, ihre <img>-Tags kriegen eine besondere Klasse und können dadurch im CSS anders dargestellt werden.

Man kann das zum Beispiel im Post über das Winkekatzen-Favicon gut sehen. Die Winkekatze ohne Rahmen hat dort einen hellen Hintergrund, die Winkekatze mit Rahmen, die sowohl bei hellen als auch bei dunklen Hintergründen funktionieren soll, hat keinen hellen Hintergrund.

Für den nicht-Darkmode ändert sich übrigens nichts.