A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="en">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>JavaScript Bloat in 2024 (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Is that even respected? Retrospectively? What a shAItshow…
  28. https://neil-clarke.com/block-the-bots-that-feed-ai-models-by-scraping-your-website/ -->
  29. <meta name="robots" content="noai, noimageai">
  30. <!-- Documented, feel free to shoot an email. -->
  31. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  32. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  36. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  37. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  38. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  39. <script>
  40. function toggleTheme(themeName) {
  41. document.documentElement.classList.toggle(
  42. 'forced-dark',
  43. themeName === 'dark'
  44. )
  45. document.documentElement.classList.toggle(
  46. 'forced-light',
  47. themeName === 'light'
  48. )
  49. }
  50. const selectedTheme = localStorage.getItem('theme')
  51. if (selectedTheme !== 'undefined') {
  52. toggleTheme(selectedTheme)
  53. }
  54. </script>
  55. <meta name="robots" content="noindex, nofollow">
  56. <meta content="origin-when-cross-origin" name="referrer">
  57. <!-- Canonical URL for SEO purposes -->
  58. <link rel="canonical" href="https://tonsky.me/blog/js-bloat/">
  59. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  60. <article>
  61. <header>
  62. <h1>JavaScript Bloat in 2024</h1>
  63. </header>
  64. <nav>
  65. <p class="center">
  66. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  67. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  68. </svg> Accueil</a> •
  69. <a href="https://tonsky.me/blog/js-bloat/" title="Lien vers le contenu original">Source originale</a>
  70. <br>
  71. Mis en cache le 2024-03-03
  72. </p>
  73. </nav>
  74. <hr>
  75. <p><em>Translations: <a href="https://habr.com/ru/companies/ruvds/articles/796595/" target="_blank">Russian</a></em></p>
  76. <p>I was a bit out of touch with modern front-end development. I also remembered articles about web bloat, how the average web page size was approaching several megabytes!</p>
  77. <p>So all this time I was living under impression that, for example, if the average web page size is 3 MB, then JavaScript bundle should be around 1 MB. Surely content should still take the majority, no?</p>
  78. <p>Well, the only way to find out is to fuck around. Let’s do a reality check!</p>
  79. <p>I’m writing this in 2024, so maybe do a sequel in a few years?</p>
  80. <h1 id="method">Method</h1>
  81. <ul>
  82. <li>Firefox on macOS (but should be the same in any browser)</li>
  83. <li>Not incognito (I want to see numbers <em>inside</em> the app, and there’s a better chance it will resemble actual everyday experience)</li>
  84. <li>All extensions disabled</li>
  85. <li>JavaScript only</li>
  86. <li>Uncompressed</li>
  87. <li>Service Workers enabled (again, more real-life)</li>
  88. <li>All caching disabled (cold load)</li>
  89. </ul>
  90. <p>Why only JavaScript? Content varies a lot from site to site (surely videos on YouTube are heavier than text messages on Slack), but JavaScript is a universal metric for “complexity of interactions”.</p>
  91. <p>The main goal is to evaluate how much work the browser has to do to parse and execute code.</p>
  92. <p>To set some baseline, let’s start with this blog:</p>
  93. <figure>
  94. <img src="https://tonsky.me/blog/js-bloat/tonsky@2x.webp?t=1709315366"> </figure>
  95. <p>The number here would be 0.004 MB. I also highlighted all the important bits you need to set if you decide to reproduce this at home.</p>
  96. <h1 id="landings">Landings</h1>
  97. <p>Okay, let’s start with something simple, like landing pages/non-interactive apps.</p>
  98. <p>A normal slightly interactive page looks like this — Wikipedia, 0.2 MB:</p>
  99. <figure>
  100. <img src="https://tonsky.me/blog/js-bloat/wikipedia@2x.webp?t=1709315366"> </figure>
  101. <p>Slightly bloated — like this — Linear, 3 MB:</p>
  102. <figure>
  103. <img src="https://tonsky.me/blog/js-bloat/linear@2x.webp?t=1709315366"> </figure>
  104. <p>Remember: that’s without images, or videos, or even styles! Just JS code.</p>
  105. <p>A bad landing page looks like this — Zoom, 6 MB:</p>
  106. <figure>
  107. <img src="https://tonsky.me/blog/js-bloat/zoom@2x.webp?t=1709315366"> </figure>
  108. <p>or like Vercel, 6 MB:</p>
  109. <figure>
  110. <img src="https://tonsky.me/blog/js-bloat/vercel@2x.webp?t=1709315366"> </figure>
  111. <p>Yes, this is just a landing page. No app, no functionality, no calls. 6 MB of JavaScript just for that.</p>
  112. <p>You can do a lot worse, though — Gitlab, 13 MB:</p>
  113. <figure>
  114. <img src="https://tonsky.me/blog/js-bloat/gitlab@2x.webp?t=1709315366"> </figure>
  115. <p>Still just the landing.</p>
  116. <h1 id="mostly-static-websites">Mostly static websites</h1>
  117. <p>Nothing simpler than showing a static wall of text. Medium needs 3 MB just to do that:</p>
  118. <figure>
  119. <img src="https://tonsky.me/blog/js-bloat/medium@2x.webp?t=1709315366"> </figure>
  120. <p>Substack needs 4 MB:</p>
  121. <figure>
  122. <img src="https://tonsky.me/blog/js-bloat/substack@2x.webp?t=1709315366"> </figure>
  123. <p>Progress?</p>
  124. <p>Quora, 4.5 MB:</p>
  125. <figure>
  126. <img src="https://tonsky.me/blog/js-bloat/quora@2x.webp?t=1709315366"> </figure>
  127. <p>Pinterest, 10 MB:</p>
  128. <figure>
  129. <img src="https://tonsky.me/blog/js-bloat/pinterest@2x.webp?t=1709315366"> </figure>
  130. <p>Patreon, 11 MB:</p>
  131. <figure>
  132. <img src="https://tonsky.me/blog/js-bloat/patreon@2x.webp?t=1709315366"> </figure>
  133. <p>And all this could’ve been a static page...</p>
  134. <h1 id="search">Search</h1>
  135. <p>When your app’s interactivity is limited to mostly search. Type the query — show the list of results. How heavy is that?</p>
  136. <p>StackOverflow, 3.5 MB:</p>
  137. <figure>
  138. <img src="https://tonsky.me/blog/js-bloat/stackoverflow@2x.webp?t=1709315366"> </figure>
  139. <p>NPM, 4 MB:</p>
  140. <figure>
  141. <img src="https://tonsky.me/blog/js-bloat/npmjs@2x.webp?t=1709315366"> </figure>
  142. <p>Airbnb, 7 MB:</p>
  143. <figure>
  144. <img src="https://tonsky.me/blog/js-bloat/airbnb@2x.webp?t=1709315366"> </figure>
  145. <p>Booking.com, 12 MB:</p>
  146. <figure>
  147. <img src="https://tonsky.me/blog/js-bloat/booking@2x.webp?t=1709315366"> </figure>
  148. <p>But Niki, booking is complicated! Look at all this UI! All these filters. All these popups about people near you stealing your vacation!</p>
  149. <p>Okay, okay. Something simpler then. Google. How about Google? One text field, list of links. Right?</p>
  150. <p>Well, it’ll cost you whooping 9 MB:</p>
  151. <figure>
  152. <img src="https://tonsky.me/blog/js-bloat/google@2x.webp?t=1709315366"> </figure>
  153. <p>Just to show a list of links.</p>
  154. <h1 id="simple-one-interaction-apps">Simple one-interaction apps</h1>
  155. <p>Google Translate is just two text boxes. For that, you need 2.5 MB:</p>
  156. <figure>
  157. <img src="https://tonsky.me/blog/js-bloat/google_translate@2x.webp?t=1709315366"> </figure>
  158. <p>ChatGPT is <em>one</em> text box. 7 MB:</p>
  159. <figure>
  160. <img src="https://tonsky.me/blog/js-bloat/openai@2x.webp?t=1709315366"> </figure>
  161. <p>I mean, surely, ChatGPT is complex. But on the server, not in the browser!</p>
  162. <h1 id="videos">Videos</h1>
  163. <p>Loom — 7 MB:</p>
  164. <figure>
  165. <img src="https://tonsky.me/blog/js-bloat/loom@2x.webp?t=1709315366"> </figure>
  166. <p>YouTube — 12 MB:</p>
  167. <figure>
  168. <img src="https://tonsky.me/blog/js-bloat/youtube@2x.webp?t=1709315366"> </figure>
  169. <p>Compare it to people who really care about performance — Pornhub, 1.4 MB:</p>
  170. <figure>
  171. <img src="https://tonsky.me/blog/js-bloat/pornhub@2x.webp?t=1709315366"> </figure>
  172. <h1 id="audio">Audio</h1>
  173. <p>I guess audio just requires 12 MB no matter what:</p>
  174. <p>SoundCloud:</p>
  175. <figure>
  176. <img src="https://tonsky.me/blog/js-bloat/soundcloud@2x.webp?t=1709315366"> </figure>
  177. <p>Spotify:</p>
  178. <figure>
  179. <img src="https://tonsky.me/blog/js-bloat/spotify@2x.webp?t=1709315366"> </figure>
  180. <h1 id="email">Email</h1>
  181. <p>Okay, video and audio are probably heavy stuff (even though we are not measuring content, just JS, remember!). Let’s move to simpler office tasks.</p>
  182. <p>Google Mail is just (just!) 20 MB:</p>
  183. <figure>
  184. <img src="https://tonsky.me/blog/js-bloat/gmail@2x.webp?t=1709315366"> </figure>
  185. <p>It’s a freaking mailbox!!! How on earth is it almost as big as Figma, who ships entire custom C++/OpenGL rendering for their app?</p>
  186. <figure>
  187. <img src="https://tonsky.me/blog/js-bloat/figma@2x.webp?t=1709315366"> </figure>
  188. <p>And if you are thinking: mail is complicated, too. Lots of UI, lots of interactivity. Maybe 20 MB is okay?</p>
  189. <p>No!</p>
  190. <p>Just no. See, FastMail, same deal, but only 2 MB. 10× less!</p>
  191. <figure>
  192. <img src="https://tonsky.me/blog/js-bloat/fastmail@2x.webp?t=1709315366"> </figure>
  193. <h1 id="productivity">Productivity</h1>
  194. <p>Okay, maybe e-mail is too complicated? How about something even simpler? Like a TODO list?</p>
  195. <p>Well, meet Todoist, 9 MB:</p>
  196. <figure>
  197. <img src="https://tonsky.me/blog/js-bloat/todoist@2x.webp?t=1709315366"> </figure>
  198. <p>Showing you a list of files in folders requires 10 MB in Dropbox:</p>
  199. <figure>
  200. <img src="https://tonsky.me/blog/js-bloat/dropbox@2x.webp?t=1709315366"> </figure>
  201. <p>List of passwords? That’ll be 13 MB on 1Password:</p>
  202. <figure>
  203. <img src="https://tonsky.me/blog/js-bloat/1password@2x.webp?t=1709315366"> </figure>
  204. <p>Cards? Add 0.5 MB more, up to 13.5 MB. Trello:</p>
  205. <figure>
  206. <img src="https://tonsky.me/blog/js-bloat/trello@2x.webp?t=1709315366"> </figure>
  207. <p>Okay, maybe TODO lists are too complex, too? How about chatting?</p>
  208. <p>Well, Discord needs 21 MB to do that:</p>
  209. <figure>
  210. <img src="https://tonsky.me/blog/js-bloat/discord@2x.webp?t=1709315366"> </figure>
  211. <h1 id="document-editing">Document editing</h1>
  212. <p>Okay, document editing is hard, right? You have to implement cursor movement, synchronization, etc.</p>
  213. <p>Google Docs, 13.5 MB:</p>
  214. <figure>
  215. <img src="https://tonsky.me/blog/js-bloat/google_docs@2x.webp?t=1709315366"> </figure>
  216. <p>Something simpler? Notion, 16 MB:</p>
  217. <figure>
  218. <img src="https://tonsky.me/blog/js-bloat/notion@2x.webp?t=1709315366"> </figure>
  219. <h1 id="social-networks">Social Networks</h1>
  220. <p>The typical size of code that social networks need for like buttons to go brrr is 12 MB.</p>
  221. <p>Twitter, 11 MB:</p>
  222. <figure>
  223. <img src="https://tonsky.me/blog/js-bloat/twitter@2x.webp?t=1709315366"> </figure>
  224. <p>Facebook, 12 MB:</p>
  225. <figure>
  226. <img src="https://tonsky.me/blog/js-bloat/facebook@2x.webp?t=1709315366"> </figure>
  227. <p>TikTok, 12.5 MB:</p>
  228. <figure>
  229. <img src="https://tonsky.me/blog/js-bloat/tiktok@2x.webp?t=1709315366"> </figure>
  230. <p>Instagram is somehow bigger than Facebook, despite having like 10× less functions. 16 MB:</p>
  231. <figure>
  232. <img src="https://tonsky.me/blog/js-bloat/instagram@2x.webp?t=1709315366"> </figure>
  233. <p>LinkedIn. Is it a blog? A platform? It has search, it has messaging, it has social functions. Anyways, that’ll be 31 MB:</p>
  234. <figure>
  235. <img src="https://tonsky.me/blog/js-bloat/linkedin@2x.webp?t=1709315366"> </figure>
  236. <p>By the way, I'd like to add you to my professional network on LinkedIn.</p>
  237. <h1 id="elephants--its-own-category">Elephants — its own category</h1>
  238. <p>Sometimes websites are so stupidly, absurdly large that they deserve their own category.</p>
  239. <p>Here, Jira, a task management software. Almost 50 MB!</p>
  240. <figure>
  241. <img src="https://tonsky.me/blog/js-bloat/jira@2x.webp?t=1709315366"> </figure>
  242. <p>Do they ship the entire Electron compiled WASM or what?</p>
  243. <p>But that’s not the limit! Slack adds 5 more MB, up to 55 MB:</p>
  244. <figure>
  245. <img src="https://tonsky.me/blog/js-bloat/slack@2x.webp?t=1709315366"> </figure>
  246. <p>Yes, it’s a chat. You know, list of users, messages, reactions. Stuff we did on raw HTML, even before JS was invented?</p>
  247. <p>That’s 55 MB in today’s world. It’s almost like they are trying to see how much more bullshit can they put in a browser before it breaks.</p>
  248. <p>Finally, this blew my mind. Somehow <a href="https://react.dev/blog/2023/03/16/introducing-react-dev" target="_blank">react.dev</a> starts with a modest 2 MB but as you scroll back and forth, it grows indefinitely. Just for fun, I got it to 100 MB (of JavaScript!), but you can go as far as you like:</p>
  249. <figure>
  250. <video autoplay="" muted="" loop="" preload="auto" playsinline="" controls="">
  251. <source src="https://tonsky.me/blog/js-bloat/react@2x.mp4?t=1708621056" type="video/mp4">
  252. </source></video>
  253. </figure>
  254. <p>What is going on there? Even if it unloads and downloads parts of that blog post, how is it growing so quickly? The text itself is probably only 50 KB (0.05 MB).</p>
  255. <p>UPD: It has been brought to my attention that this behavior is not, in fact, representative of normal user experience. Normally embedded code editors will be cached after first load and subsequent loads will be served from disk cache. So as you scroll, you will see no network traffic, but these 100 MB of JS will still be parsed, evaluated and initialized over and over as you scroll.</p>
  256. <h1 id="how-fast-are-we-degrading">How fast are we degrading?</h1>
  257. <p>Look how cute! In 2015 average web page size was approaching shareware version of Doom 1 (2.5 MB):</p>
  258. <figure>
  259. <img src="https://tonsky.me/blog/js-bloat/bloat_2015@2x.webp?t=1709315366"><figcaption><a href="https://twitter.com/xbs/status/626781529054834688" target="_blank">Source</a></figcaption> </figure>
  260. <p>Well, in 2024, Slack pulls up 55 MB, the size of the original Quake 1 with all the resources. But now it’s just in JavaScript alone.</p>
  261. <p>For a chat app!</p>
  262. <h1 id="how-big-is-10-mb-anyway">How big is 10 MB anyway?</h1>
  263. <p>To be honest, after typing all these numbers, 10 MB doesn’t even feel that big or special. Seems like shipping 10 MB of <em>code</em> is normal now.</p>
  264. <p>If we assume that the average code line is about 65 characters, that would mean we are shipping ~150,000 lines of code. With every website! Sometimes just to show static content!</p>
  265. <p>And that code is minified already. So it’s more like 300K+ LoC just for one website.</p>
  266. <p>But are modern websites really <em>that</em> complex? The poster child of SPAs, Google Maps, is quite modest by modern standards — is <em>still</em> just 4.5 MB:</p>
  267. <figure>
  268. <img src="https://tonsky.me/blog/js-bloat/google_maps@2x.webp?t=1709315366"> </figure>
  269. <p>Somebody at Google is seriously falling behind. Written with modern front-end technologies, it should be at least 20 MB.</p>
  270. <p>And if you, like me, thought that “Figma is a really complex front-end app, so it must have a huge javascript download size”, well, that’s correct, but then Gmail is about as complex as Figma, LinkedIn is 1.5× more complex and Slack is 2.5× more ¯\_(ツ)_/¯</p>
  271. <h1 id="conclusion">Conclusion</h1>
  272. <p>It’s not just about download sizes. I welcome high-speed internet as much as the next guy. But code — JavaScript — is something that your browser has to parse, keep in memory, execute. It’s not free. And these people talk about performance and battery life...</p>
  273. <p>Call me old-fashioned, but I firmly believe content should outweigh code size. If you are writing a blog post for 10K characters, you don’t need 1000× more JavaScript to render it.</p>
  274. <p>This site is doing it right:</p>
  275. <figure>
  276. <img src="https://tonsky.me/blog/js-bloat/jquery@2x.webp?t=1709315366"> </figure>
  277. <p>That’s 0.1 MB. And that’s enough!</p>
  278. <p>And yet, on the same internet, in the same timeline, Gitlab needs 13 MB of code, 500K+ LoC of JS, just to display a static landing page.</p>
  279. <p>Fuck me.</p>
  280. </article>
  281. <hr>
  282. <footer>
  283. <p>
  284. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  285. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  286. </svg> Accueil</a> •
  287. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  288. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  289. </svg> Suivre</a> •
  290. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  291. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  292. </svg> Pro</a> •
  293. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  294. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  295. </svg> Email</a> •
  296. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  297. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  298. </svg> Légal</abbr>
  299. </p>
  300. <template id="theme-selector">
  301. <form>
  302. <fieldset>
  303. <legend><svg class="icon icon-brightness-contrast">
  304. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  305. </svg> Thème</legend>
  306. <label>
  307. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  308. </label>
  309. <label>
  310. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  311. </label>
  312. <label>
  313. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  314. </label>
  315. </fieldset>
  316. </form>
  317. </template>
  318. </footer>
  319. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  320. <script>
  321. function loadThemeForm(templateName) {
  322. const themeSelectorTemplate = document.querySelector(templateName)
  323. const form = themeSelectorTemplate.content.firstElementChild
  324. themeSelectorTemplate.replaceWith(form)
  325. form.addEventListener('change', (e) => {
  326. const chosenColorScheme = e.target.value
  327. localStorage.setItem('theme', chosenColorScheme)
  328. toggleTheme(chosenColorScheme)
  329. })
  330. const selectedTheme = localStorage.getItem('theme')
  331. if (selectedTheme && selectedTheme !== 'undefined') {
  332. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  333. }
  334. }
  335. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  336. window.addEventListener('load', () => {
  337. let hasDarkRules = false
  338. for (const styleSheet of Array.from(document.styleSheets)) {
  339. let mediaRules = []
  340. for (const cssRule of styleSheet.cssRules) {
  341. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  342. continue
  343. }
  344. // WARNING: Safari does not have/supports `conditionText`.
  345. if (cssRule.conditionText) {
  346. if (cssRule.conditionText !== prefersColorSchemeDark) {
  347. continue
  348. }
  349. } else {
  350. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  351. continue
  352. }
  353. }
  354. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  355. }
  356. // WARNING: do not try to insert a Rule to a styleSheet you are
  357. // currently iterating on, otherwise the browser will be stuck
  358. // in a infinite loop…
  359. for (const mediaRule of mediaRules) {
  360. styleSheet.insertRule(mediaRule.cssText)
  361. hasDarkRules = true
  362. }
  363. }
  364. if (hasDarkRules) {
  365. loadThemeForm('#theme-selector')
  366. }
  367. })
  368. </script>
  369. </body>
  370. </html>