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 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  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>How I fell in love with low-js (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. <!-- Documented, feel free to shoot an email. -->
  28. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  29. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  30. <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>
  31. <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>
  32. <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>
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  36. <script>
  37. function toggleTheme(themeName) {
  38. document.documentElement.classList.toggle(
  39. 'forced-dark',
  40. themeName === 'dark'
  41. )
  42. document.documentElement.classList.toggle(
  43. 'forced-light',
  44. themeName === 'light'
  45. )
  46. }
  47. const selectedTheme = localStorage.getItem('theme')
  48. if (selectedTheme !== 'undefined') {
  49. toggleTheme(selectedTheme)
  50. }
  51. </script>
  52. <meta name="robots" content="noindex, nofollow">
  53. <meta content="origin-when-cross-origin" name="referrer">
  54. <!-- Canonical URL for SEO purposes -->
  55. <link rel="canonical" href="https://edofic.com/posts/2022-01-28-low-js/">
  56. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  57. <article>
  58. <header>
  59. <h1>How I fell in love with low-js</h1>
  60. </header>
  61. <nav>
  62. <p class="center">
  63. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  64. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  65. </svg> Accueil</a> •
  66. <a href="https://edofic.com/posts/2022-01-28-low-js/" title="Lien vers le contenu original">Source originale</a>
  67. </p>
  68. </nav>
  69. <hr>
  70. <p><time>2022-01-28</time></p>
  71. <hr>
  72. <p>About 5 years ago I was working on a project written in style still common for today:</p>
  73. <ul>
  74. <li>python backend serving a REST API (public btw)</li>
  75. <li>SPA using framework-of-the day</li>
  76. </ul>
  77. <p>And it was horrible. Not to mention that the framework-of-the-day did not actually take off and we ended up using something very fringe - there were real performance and complexity problems. And to this day I believe all our problems stemmed from the base architecture. There were just too many layers, each contributing overhead. Both in execution speed (aka “app is slow”) and development speed (“our velocity is too low”).</p>
  78. <p>Let’s look at a more concrete example. What happened for a typical page load (assuming navigation, otherwise it starts by loading multiple mb of js)</p>
  79. <ul>
  80. <li>JS router detects navigation and updates components</li>
  81. <li>components fetch needed data</li>
  82. <li>this turns out to be tens (sometimes hundreds!) of requests because the general API is very normalized (yes we were discussing GraphQL at this point)</li>
  83. <li>then throw most of this data (95%+) away since we don’t <em>really</em> need all those fields (yes yes, GraphQL again…)</li>
  84. <li>render templates in JS with the processed data</li>
  85. <li>shove the results in the DOM</li>
  86. </ul>
  87. <p>…meanwhile the user is impatiently twiddling their thumbs. Sounds familiar? Because this was by far not the only app I’ve seen with this problem. Surely we can do better…</p>
  88. <p>Luckily I had a colleague with Ruby on Rails background. And he was a big fan. Quite loud about how plain old server side rendered pages were much simpler and faster too. It all made a lot of sense so I made a prototype. The above flow was now:</p>
  89. <ul>
  90. <li>flask router directs a request to a controller</li>
  91. <li>controller constructs a query (we already had a query and serialization layer available)</li>
  92. <li>exactly the data that is needed (column selection) is fetched</li>
  93. <li>then passed through a presentation layer that makes the view models</li>
  94. <li>jinja template is rendered</li>
  95. <li>html returned to the browser</li>
  96. </ul>
  97. <p>And I be damned, it was faster! A lot faster. If you think about it it makes a lot of sense. It looks like about the same just looking at the line items but it’s actually performing a lot less work and more importantly there is less communication (and thus latency). And the best part? It was also a lot faster to work with. Mostly due to fewer moving parts, so each change needed to touch less places. After all I managed to cobble the prototype together in a single day.</p>
  98. <p>So we went all in on this approach and shipped (a much better version of this) to production to replace a core component of the product (as an opt-in v2 - I’m not completely insane…). Smashing hit. Feature parity in no time, even new feature were added very quickly. Less bugs, better performance, happier customers.</p>
  99. <p>So where was the problem? Sounds like a tall-tale not a real story from the trenches. Well there were hacks. Like “preserving” the scroll position. Or tracking whether a modal is open in the url. But the real issue was the user experience. The fidelity was much lower. Everything looked a bit more crude, some things like switching “tabs” took surprisingly long (obviously since it was a full page refresh) or there was jarring page blinking. Where is my autocomplete? Where are the smooth transitions? Where has my multiselect choice gone, I just change the filter a bit?</p>
  100. <p>The technology (even just <a href="https://github.com/turbolinks/turbolinks">turbolinks</a> would be a big improvement) to fix all this with minimal effort was there (I now checked release years) but my mindset wasn’t. Instead I claimed, drunk on success (it really was a big success) and full of hubris, that this is the tradeoff we made to achieve all other goals.</p>
  101. <p>But surely we can do better?</p>
  102. <h1 id="a-few-years-later">A few years later</h1>
  103. <p>Different project, different company, now focusing on purely backend services. Maybe free(-er) of my prejudices? I come across <a href="https://htmx.org/">htmx</a> and it was love at first sight. It dawned on me it’s actually quite simple to make an http request in JS and shove the resulting html into the DOM. But it’s almost trivial to do it declaratively with a small library. But by then I moved firmly into <a href="http://boringtechnology.club/">Choose boring technology</a> camp (still here) and this looked way too fringe.</p>
  104. <p>But then after a while I see Basecamp launch <a href="https://www.hey.com/">hey.com</a> and claim they’re doing the whole product using this approach. Even open sourced <a href="https://hotwired.dev/">the library - Hotwired</a>.</p>
  105. <p>But the real kicker was seeing <a href="https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision">Rails 7 launch</a> with Hotwired as the default “frontend framework”. So maybe this approach is going (near) mainstream after all? Indeed htmx now matches some second-tier JS framworks in terms of stars, heck it surpassed our framework-of-the-day. And there’s a proliferation of tools as well. Just I few honorable mentions before moving on:</p>
  106. <h1 id="what-am-i-even-talking-about">What am I even talking about?</h1>
  107. <p>Basic premise is: browsers are great at navigating pages, let’s leverage that by serving users directly with HTML. This already gives you half-decent pages but you can go further and sprinkle on some sugar. A bit of JS. Instead of reinventing the world in JS let’s try to really use what modern browsers provide natively and then push just a bit forward.</p>
  108. <p>For starters: a lot can be done natively nowadays - <a href="http://youmightnotneedjs.com/">http://youmightnotneedjs.com/</a></p>
  109. <p>But more concretely I’m talking about taking Django/Rails/Buffalo/Play, writing a plain old server side rendered application and shoving in Htmx/Turbo/Unpoly for that little bit of interactivy that gives you things like</p>
  110. <ul>
  111. <li>immediate form validation without submitting</li>
  112. <li>modals that preserve app state (scroll positions, input fields) underneath</li>
  113. <li>responsive facet search</li>
  114. <li>pagination without page reloads</li>
  115. <li>infinite scroll</li>
  116. </ul>
  117. <p>Why now just build all of this in React? Because you can vastly simplify the architecture, reduce the number of moving parts and thus boilerplate and most importantly the total effort/time to do something.</p>
  118. <p>If I steal some images from the <a href="http://triskweline.de/unpoly-rugb">Unpoly Story</a> you don’t want to program like it’s 2015</p>
  119. <p><img src="/images/low-js/2015.webp" alt="stack in 2015"></p>
  120. <p>But instead more like it’s <strong>2005</strong></p>
  121. <p><img src="/images/low-js/2005.webp" alt="stack in 2005"></p>
  122. <p>But maybe with a bit less random JS and keeping it declarative where possible. Because the main thesis is that the fidelity of the user experience follows the Pareto principle, you can get say 80% of the experience for say 20% of work.</p>
  123. <p><img src="/images/low-js/tradeoff.webp" alt="tradeoff"></p>
  124. <p>With the imporant caveat that this does not apply to applications where heavy and smooth interaction is one of the core features. E.g. games.</p>
  125. <h1 id="my-experience">My experience</h1>
  126. <p>I did a toy project in Go using <a href="https://github.com/gorilla/mux">gorilla/mux</a> for routing and <a href="https://github.com/stevelacy/daz">daz</a> for templates (this one is quite out there…) with <a href="https://htmx.org/">htmx</a> for interactivity and <a href="https://picocss.com/">Pico CSS</a> for styling (I wrote <a href="/posts/2022-01-18-tailwind-vs-pico/">a post</a> about this part). Absolutely no javascript, miniscule amount of css, everything else written in Go.</p>
  127. <p>A stark contrast to <a href="/posts/2021-12-19-trying-out-remix/">Trying out Remix</a> where everything is JS. And I preferred it. Felt more productive. I guess this is an instance of <a href="https://en.wikipedia.org/wiki/Conway%27s_law">Conway’s Law</a> in action: frontend engineers doing the full stack prefer frontend focused tools that do backend while backend engineers prefer backend tools that do frontend. But I think there’s a technical tradeoff here as well. If your app is predominantly focused on a slick experience and you can get by with a limited backend then something like Remix is a great tool. Conversely if you’re more backend heavy and you need just a <em>slick enough</em> frontend then low-js approach suits you better. My hot take here is that most business apps fall into the latter category.</p>
  128. <p>But what’s the actual experience like? In one word: boring. In a good way. Just like doing web pages way back when Rails was the new hot thing. Htmx mostly just gets out of the way. You sprinkle a few attributes here and there and things mostly just work.</p>
  129. <p>The part I was most worried about was performance. But as far as I can see it’s better that with an SPA for most of my cases. Hear me out:</p>
  130. <ul>
  131. <li>initial page load is super fast as it’s just loading the page that is server rendered any way</li>
  132. <li>only loading 10kb of js (htmx) and even that is at the end for interactivity</li>
  133. <li>when clicking around things load in ~150ms from another region (20ms on localhost since computers are fast if you’re not wasting cycles)</li>
  134. <li>most of it (read requests) is safe to preload on mouse down and then navigate as usual on mouse up. Since the click <a href="http://instantclick.io/click-test">takes 100ms+</a> this makes navigation feel instant. Trivial to do via <a href="https://htmx.org/extensions/preload/">preload extension</a>.</li>
  135. <li>slow posts can still be <a href="https://htmx.org/attributes/hx-indicator/">hidden a spinner declaratively</a></li>
  136. </ul>
  137. <p>All in all I think I found my cure for javascript fatigue as well as a preferred way to do new projects going forward.</p>
  138. <hr>
  139. <p>Last modified on 2022-01-28</p>
  140. </article>
  141. <hr>
  142. <footer>
  143. <p>
  144. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  145. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  146. </svg> Accueil</a> •
  147. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  148. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  149. </svg> Suivre</a> •
  150. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  151. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  152. </svg> Pro</a> •
  153. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  154. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  155. </svg> Email</a> •
  156. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  157. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  158. </svg> Légal</abbr>
  159. </p>
  160. <template id="theme-selector">
  161. <form>
  162. <fieldset>
  163. <legend><svg class="icon icon-brightness-contrast">
  164. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  165. </svg> Thème</legend>
  166. <label>
  167. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  168. </label>
  169. <label>
  170. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  171. </label>
  172. <label>
  173. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  174. </label>
  175. </fieldset>
  176. </form>
  177. </template>
  178. </footer>
  179. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  180. <script>
  181. function loadThemeForm(templateName) {
  182. const themeSelectorTemplate = document.querySelector(templateName)
  183. const form = themeSelectorTemplate.content.firstElementChild
  184. themeSelectorTemplate.replaceWith(form)
  185. form.addEventListener('change', (e) => {
  186. const chosenColorScheme = e.target.value
  187. localStorage.setItem('theme', chosenColorScheme)
  188. toggleTheme(chosenColorScheme)
  189. })
  190. const selectedTheme = localStorage.getItem('theme')
  191. if (selectedTheme && selectedTheme !== 'undefined') {
  192. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  193. }
  194. }
  195. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  196. window.addEventListener('load', () => {
  197. let hasDarkRules = false
  198. for (const styleSheet of Array.from(document.styleSheets)) {
  199. let mediaRules = []
  200. for (const cssRule of styleSheet.cssRules) {
  201. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  202. continue
  203. }
  204. // WARNING: Safari does not have/supports `conditionText`.
  205. if (cssRule.conditionText) {
  206. if (cssRule.conditionText !== prefersColorSchemeDark) {
  207. continue
  208. }
  209. } else {
  210. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  211. continue
  212. }
  213. }
  214. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  215. }
  216. // WARNING: do not try to insert a Rule to a styleSheet you are
  217. // currently iterating on, otherwise the browser will be stuck
  218. // in a infinite loop…
  219. for (const mediaRule of mediaRules) {
  220. styleSheet.insertRule(mediaRule.cssText)
  221. hasDarkRules = true
  222. }
  223. }
  224. if (hasDarkRules) {
  225. loadThemeForm('#theme-selector')
  226. }
  227. })
  228. </script>
  229. </body>
  230. </html>