瀏覽代碼

Links

master
David Larlet 1 年之前
父節點
當前提交
1d71edc555
簽署人: David Larlet <david@larlet.fr> GPG Key ID: 3E2953A359E7E7BD

+ 336
- 0
cache/2023/2074a4d527220f5ddf2dc0b4e678c83a/index.html 查看文件

@@ -0,0 +1,336 @@
<!doctype html><!-- This is a valid HTML5 document. -->
<!-- Screen readers, SEO, extensions and so on. -->
<html lang="fr">
<!-- Has to be within the first 1024 bytes, hence before the `title` element
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
<meta charset="utf-8">
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
<!-- The viewport meta is quite crowded and we are responsible for that.
See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- Required to make a valid HTML5 document. -->
<title>Classic rock, Mario Kart, and why we can’t agree on Tailwind (archive) — David Larlet</title>
<meta name="description" content="Publication mise en cache pour en conserver une trace.">
<!-- That good ol' feed, subscribe :). -->
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
<!-- Generated from https://realfavicongenerator.net/ such a mess. -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
<link rel="manifest" href="/static/david/icons2/site.webmanifest">
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
<meta name="msapplication-TileColor" content="#f7f7f7">
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
<!-- Documented, feel free to shoot an email. -->
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
<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>
<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>
<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>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<script>
function toggleTheme(themeName) {
document.documentElement.classList.toggle(
'forced-dark',
themeName === 'dark'
)
document.documentElement.classList.toggle(
'forced-light',
themeName === 'light'
)
}
const selectedTheme = localStorage.getItem('theme')
if (selectedTheme !== 'undefined') {
toggleTheme(selectedTheme)
}
</script>

<meta name="robots" content="noindex, nofollow">
<meta content="origin-when-cross-origin" name="referrer">
<!-- Canonical URL for SEO purposes -->
<link rel="canonical" href="https://joshcollinsworth.com/blog/tailwind-is-smart-steering">

<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">


<article>
<header>
<h1>Classic rock, Mario Kart, and why we can’t agree on Tailwind</h1>
</header>
<nav>
<p class="center">
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="https://joshcollinsworth.com/blog/tailwind-is-smart-steering" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p>When my brother and I were younger, our tastes in music were nearly polar opposites. I was a total hipster, and valued what I saw as artistic integrity and creative innovation. I was almost entirely into new and active, of-the-moment artists.</p>
<p>In contrast, my brother made virtually no judgments at all. He was open to anything and everything, but his mainstays centered on classic rock of the ’70s and ’80s. He loved rediscovering decades-old albums by many of the most well-known artists of their time.</p>
<p>Like any good elitist snob, I hated anything popular. So naturally, my brother’s kind of music was anathema to me at the time.</p>
<p>My brother never really understood what it was I seemed to dislike so strongly. So finally, one day, he asked me.</p>
<p>I thought about the question for a minute, and then summarized: “it’s all style and no substance.”</p>
<p>He burst out laughing.</p>
<p>“That’s the <em>best</em> thing about it!” he said with a huge grin. “That’s the whole point!”</p>
<p>I knew we looked at the same things in opposite ways. But I had never realized before then that, wildly enough, we had <em>all the exact same reasons</em> for it.</p>
<h2 id="we-dont-disagree-where-we-think-we-do"><a aria-hidden="true" tabindex="-1" href="#we-dont-disagree-where-we-think-we-do"><span class="icon icon-link"></span></a>We don’t disagree where we think we do</h2>
<p>Tailwind is nearly as ubiquitous as it is polarizing these days. (I trust if you’ve been interested enough to read this far, I don’t need to cite any sources there.)</p>
<p>Proponents show a near cult-like devotion to Tailwind, some even going so far as to claim it’s “fixed” CSS—or at the very least, made it manageable and predictable in a way it wasn’t for them before. Most frontend frameworks and products feature a first-class Tailwind integration, due to its soaring popularity.</p>
<p>Detractors, on the other hand, claim Tailwind is messy; it gets in the way; it violates core fundamentals of web development; it cuts against the grain; it diminishes the power of CSS; and/or that it does the <em>opposite</em> of what it’s supposed to, adding complexity and making projects even <em>harder</em> to maintain.</p>
<p>Many who use Tailwind never want to go back; many who <em>don’t</em> never want to.</p>
<p>How could we possibly disagree so sharply?</p>
<p>After using Tailwind for a good while now—both professionally and on a few small personal projects—I’ve come to what might just be the most unpopular opinion of all, in regards to that question:</p>
<p><strong>Both sides are right.</strong></p>
<p>…Ok, ok, I can hear the boos. Nobody likes a centrist. But hear me out.</p>
<p>We won’t ever get anywhere debating the merits or flaws of Tailwind, for the same reason my brother and I never persuaded each other on music.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>We could’ve dissected Bon Jovi and Sufjan Stevens all we wanted. But it didn’t matter, because our disagreement ultimately started before either one of us ever pressed the play button.</p>
</aside>

<p class="callout svelte-rzjeng"></p>
<p>We could’ve dissected Bon Jovi and Sufjan Stevens all we wanted. But it didn’t matter, because our disagreement ultimately started before either one of us ever pressed the play button.</p>

<p>It turns out, he and I actually saw every artist and album in pretty much exactly the same way to begin with.</p>
<p>We just never agreed on what was a feature, and what was a bug.</p>
<p>Likewise, I suspect most people on opposing sides of the Tailwind debate actually complete agree on Tailwind itself. I don’t think we disagree on atomic CSS, or utility classes; I think our contention comes from the valuations we made long before we ever chose our tools. Where one of us sees a selling point, the other sees a flaw.</p>
<p>Tailwind <em>is</em> great.</p>
<p>Tailwind is <em>also</em> a bad idea.</p>
<p>And it is both, at the same time, for exactly the same reasons.</p>

<p>If you’ve played <a href="https://www.nintendo.com/store/products/mario-kart-8-deluxe-switch/" rel="nofollow">Mario Kart 8 Deluxe</a> on Switch, you might know the game offers a feature called <a href="https://mariokart.fandom.com/wiki/Smart_Steering" rel="nofollow">Smart Steering</a>.</p>
<p>(<em>Yes, this is still about Tailwind. Bear with me here</em>.)</p>
<p>Smart Steering is essentially an AI copilot. When enabled, you control your racer as normal, except that if you start to go off the road, the game will automatically take the wheel and steer you back on course. No more slogging through the grass, or plummeting off the side of the track.</p>
<p>Smart Steering can help even the playing field among any mixed group (particularly on higher engine classes, where even very good players may find themselves hurling off the track).</p>
<p>But as you get better and better at the game, Smart Steering begins to help less and less…until eventually, it starts getting in the way instead.</p>
<p><img src="/images/post_images/impact.png" alt="A chart demonstrating the above paragraph; as skill level increases, the impact of usage goes from entirely positive to entirely negative."></p>
<p>Smart Steering will prevent you from taking shortcuts, for one thing; it can’t tell <em>why</em> you’re going off the main road, but it jumps in the way and prevents you from doing so regardless. It may even intervene unexpectedly, pushing you off your intended course at inopportune moments.</p>
<p>You might have a strategic reason in mind for going off the road (maybe you were avoiding a ruinous obstacle, for example), but Smart Steering won’t allow it regardless.</p>
<p>Plus, as a balancing feature, the game’s strongest speed boosts are disabled for players utilizing Smart Steering.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>The more skilled you are, the more this feature designed to augment your abilities instead begins to inhibit them. But whether it makes you better or worse—and to what degree—will depend entirely on where you’re coming from.</p>
</aside>

<p class="callout svelte-rzjeng"></p>
<p>The more skilled you are, the more this feature designed to augment your abilities instead begins to inhibit them. But whether it makes you better or worse—and to what degree—will depend entirely on where you’re coming from.</p>

<p>I’m a very good Mario Kart player (<em>I spent some thirty years of my life playing Mario Kart games before Smart Steering came along, after all</em>), and so I can’t stand Smart Steering. I never use it. I might not notice it a lot of the time—it might even occasionally help me—but what stands out to me are the times it jumps in my way at the worst possible moments.</p>
<p>Smart Steering might keep me on the track, but it <em>also</em> suppresses my fullest abilities. It thwarts the best outcomes just as commonly as it averts the worst disasters.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>To those sufficiently skilled with CSS, Tailwind feels like being forced to code with Smart Steering on.</p>
</aside>

<p class="callout svelte-rzjeng"></p>
<p>To those sufficiently skilled with CSS, Tailwind feels like being forced to code with Smart Steering on.</p>

<p>That’s why any conversation I have with somebody who likes Tailwind will probably be like trying to explain why I don’t like Smart Steering to a Mario Kart player who relies on it.</p>
<blockquote><p>“I don’t like that it keeps me from driving off the track.”</p>
<p>“What!? Why would you ever want to do that!?”</p>
<p>“You can actually skip ahead if you use this trick right here…”</p>
<p>“Ugh, what a hacky workaround. It’s really best practice to stay on the track, you know.”</p>
<p>“Look, I understand why you’re saying that, but I’ve been doing this for a long time, and I know what I’m doing.”</p>
<p>“I just don’t know why you’d make things harder on yourself, only to do something you’re not supposed to do anyway.”</p></blockquote>
<div class="side-note svelte-1j8wjk4"><p>I realize I might sound rather arrogant in choosing this comparison. Mario Kart is a racing game, after all, which might seem to imply that I think of myself as a winner, and people who don’t do things my way as inferior.</p>
<p>That’s not at all my intent; I only mean to point out that if Tailwind is your thing, it’s probably because we’re coming from different places, and like to play the game differently, for our own very valid reasons.</p>
</div>
<h2 id="there-are-no-benefits-without-tradeoffs"><a aria-hidden="true" tabindex="-1" href="#there-are-no-benefits-without-tradeoffs"><span class="icon icon-link"></span></a>There are no benefits without tradeoffs</h2>
<p>For the most part, it’s perfectly fine if we see the same thing in two different, even opposite ways. However, there’s one universal truth I think it’s important we agree on as common ground, namely: <em>benefits always come with tradeoffs</em> (and vice versa).</p>
<p>When it comes to frameworks, at least (CSS or otherwise), any place things seem simpler, we’re actually just experiencing the <em>upside</em> of some architectural decision. That decision, whatever it was, has downsides elsewhere; some other part of the chain, further away from our view, is now also <em>more</em> complex or difficult than it used to be.</p>
<p>Don’t get me wrong; that’s still usually a good thing. The new set of tradeoffs is often preferable to the old set; we wouldn’t use frameworks if that weren’t the case.</p>
<p>But complexity always exists, even if it’s remanded to a place you don’t regularly experience it.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>Problems in tech don’t <em>vanish</em>; they simply get reconfigured into new shapes, with new pointy edges in new places.</p>
</aside>

<p class="callout svelte-rzjeng"></p>
<p>Problems in tech don’t <em>vanish</em>; they simply get reconfigured into new shapes, with new pointy edges in new places.</p>

<p>So we should be incredibly skeptical of any framework (any <em>product</em>, really) that claims to have eliminated complexity—destroyed it, turned it into nothing—without being honest about the byproducts.</p>
<p>Ok, fine. Why is this all important?</p>
<p>Because Tailwind is often marketed (and spoken of by many of its proponents) as a framework that’s achieved this impossible task of obliterating CSS’s complexity from existence.</p>
<p>If you would make this argument, I would firmly dissent. But I would also agree you’re probably right <em>in your case</em>.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>You likely don’t work in the areas where the tradeoffs are, so they might seem like they don’t exist to you.</p>
</aside>

<p class="callout svelte-rzjeng"></p>
<p>You likely don’t work in the areas where the tradeoffs are, so they might seem like they don’t exist to you.</p>

<p>Boring and lazily centrist as it may sound: I think we have to agree that Tailwind and vanilla CSS are both unique sets of tradeoffs. Whether we consider one better than the other will depend very much on where we spend our time, what we value, and what we ultimately consider to be <em>the point</em> of all of this.</p>
<p>Let’s be fair: CSS <em>does</em> have myriad complexities and pitfalls, and Tailwind admittedly smooths out many of those rough edges, in various ways.</p>
<p>But CSS is <em>also</em> an incredibly powerful programming language, which Tailwind diminishes, creating liabilities of its own along the way.</p>
<p>The only <em>invalid</em> point of view is that you can have all benefits and no downsides. It just doesn’t work that way.</p>
<p><em>It all depends™.</em></p>
<h2 id="the-divide-where-do-you-want-to-be"><a aria-hidden="true" tabindex="-1" href="#the-divide-where-do-you-want-to-be"><span class="icon icon-link"></span></a>The divide: where do you want to be?</h2>
<p>So if what we actually disagree on is what we value, that naturally begs the question: where does this divide come from?</p>
<p>To me, the gap between Tailwind supporters and critics comes down to what part of the job they consider to be the most important, or perhaps, which part they just prefer to focus on.</p>
<p>Because continuing to come up with synonyms for supporters and detractors is tedious, for the sake of this section, I propose we give the opposing groups names. These names will, in themselves, be coarse generalizations, which means they will have obvious outliers and glaring contradictions. But for the sake of a model that is hopefully useful despite its flaws:</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>Let’s call the generally pro-Tailwind group <em>Builders</em>, and let’s call the generally anti-Tailwind group <em>Crafters</em>.</p>
</aside>

<p class="callout svelte-rzjeng"></p>
<p>Let’s call the generally pro-Tailwind group <em>Builders</em>, and let’s call the generally anti-Tailwind group <em>Crafters</em>.</p>

<p>This isn’t to say that Crafters don’t build things, or that the Builders aren’t skilled craftspeople. But as a quick and messy shorthand, let’s go with it for a moment, because I think it hints at the values of these two groups.</p>
<h3 id="builders"><a aria-hidden="true" tabindex="-1" href="#builders"><span class="icon icon-link"></span></a>Builders</h3>
<p>Builders clearly value getting the work done as quickly and efficiently as possible. They are making something—likely something with parts beyond the frontend—and are eager to see it through to completion.</p>
<p>Builders tend to tout Tailwind as the saving grace that got rid of the hard parts of CSS, after all. They like that Tailwind makes the work tidy and fast.</p>
<p>That, in turn, strongly implies Builders value getting through this particular part of their work as quickly and easily as possible. (We all value efficient productivity, of course. But <em>where</em> you value it may implicitly say something about your priorities.)</p>
<p>This also implies one or both of the following: either a) that they found this work overly challenging before; and/or b) that this is not the part of their job they wish to be challenged in. They likely don’t shy away from challenge; they probably just prefer it in different form.</p>
<p>Again: exceptions and outliers will be plentiful, of course. But: as a group, Builders tend to be people who’ve spent their careers, if not in other parts of the stack, then at least in other areas of frontend. That is: for most Builders (though not all), CSS is not a specialty—or at any rate, not a priority. Many people who like Tailwind are also very good at CSS, but those Builders tend to be bringing a more balanced approach, where they use Tailwind for the broad strokes of utility classes, and heavily customize the config file and/or write their own CSS to fill in the gaps.</p>
<h3 id="crafters"><a aria-hidden="true" tabindex="-1" href="#crafters"><span class="icon icon-link"></span></a>Crafters</h3>
<p>On the other side, the Crafters tend to be seasoned CSS specialists, and almost always enjoy the part of the work that Tailwind is supposed to make easier. It’s fair to say they’ve overcome the challenge presented by CSS—or, at least, that this is where they <em>like</em> to be challenged.</p>
<p>Crafters may be building holistic products and projects, just like Builders. But Crafters generally are less focused on <em>getting through</em> the frontend as a <em>part</em> of that work, and instead see the frontend as <em>the product itself</em>.</p>
<p>Because of their skills with (or willingness to be challenged by) CSS, Crafters often see Tailwind as a blunt instrument that dampens their abilities. At worst, Tailwind locks off the best parts of both CSS and of their jobs. (Tailwind can’t keep up with new CSS features, and even where it can, it can’t implement everything or do everything that CSS can.) At best, it represents a hefty learning curve, just to get back to where they already are.</p>
<p>Speed isn’t an issue for Crafters—or at least, not a priority; they are more concerned with the handiwork of their product. Styling is exactly where they want to be, because they value doing the work uniquely well over doing it well enough. Again, they see this work as central and defining to the product, and not just a detail of it.</p>
<h3 id="builders-and-crafters"><a aria-hidden="true" tabindex="-1" href="#builders-and-crafters"><span class="icon icon-link"></span></a>Builders and Crafters</h3>
<p>As <a href="https://adactio.com/journal/18982" rel="nofollow">Jeremy Keith put it so well</a>: where it comes to styling, Builders want imperative programming; they want to specify what they want, where they want, how they want it. No surprises.</p>
<p>Crafters instead want declarative programming; they understand how to wield the power of creating rules of governance within a complex system, and wish to use that power, rather than <a href="https://buildexcellentwebsit.es/" rel="nofollow">micromanaging the browser</a>.</p>
<p>Both Builders and Crafters have fully valid points of view. And in fairness, as I implied earlier, there exists a rich gradient of options between the two. You can balance workmanship and craftsmanship. You can use Tailwind only as much as you choose, and opt to reach for hand-authored CSS the rest of the time.</p>
<p>The question is really just what you value, and where you want to spend your time.</p>
<h2 id="conclusion-my-point-of-view-on-tailwind"><a aria-hidden="true" tabindex="-1" href="#conclusion-my-point-of-view-on-tailwind"><span class="icon icon-link"></span></a>Conclusion: my point of view on Tailwind</h2>
<p>To continue using the distinctions above: I consider myself a Crafter. I am, therefore, wary of any tool that nudges me towards building things a certain way—<em>especially</em> when it’s proliferated across my profession, and has become so popular you can tell most websites built with it at a glance.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>In my view, the more you optimize for building quickly, the more you optimize for homogeneity.</p>
</aside>

<p class="callout svelte-rzjeng"></p>
<p>In my view, the more you optimize for building quickly, the more you optimize for homogeneity.</p>

<p>I acknowledge that Tailwind might be a great solution, if its downsides are not issues for you, or if you have other means of mitigating them.</p>
<p>If it solves your problems, by all means, disregard my qualms as the ramblings of an old man yelling at a Tailwind-shaped cloud. It’s only a tool, after all. It shouldn’t be something we have to agree on if we’re not working together, let alone a pillar of anyone’s identity.</p>
<p>But to me, Tailwind <em>is</em> a problem in and of itself. The tradeoffs may be a net benefit for your use cases, and if so, that’s great. I’ve tested Tailwind pretty thoroughly, however, and I’ve concluded that it is <em>not</em> a net positive in my case. I don’t mind working with it on a team (it <em>is</em> useful as a unifying system, after all), but at the very least, I request the freedom to break out of it at my discretion as needed and as it’s useful. I feel <em>anyone</em> working with Tailwind should have this autonomy. I’d even go so far as to say you’re suppressing your Crafters if you <em>don’t</em> allow them that.</p>
<p>I do not care how fast I build, or how easily I prototype, so much as I care that I am building something uniquely good, and building it the right way.</p>
<p>Besides: I would argue that the last few years of growth in browser CSS, as well as frontend frameworks, have rendered Tailwind’s benefits largely moot for many use cases. We have <a href="https://fullystacked.net/posts/scope-in-css/" rel="nofollow"><code>@scope</code> in CSS now</a>. We have <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@layer" rel="nofollow">cascade layers</a>. Even if you don’t want to reach for those from the platform, most frontend frameworks (like <a href="https://kit.svelte.dev/" rel="nofollow">SvelteKit</a>, <a href="https://vuejs.org" rel="nofollow">Vue</a>, and <a href="https://astro.build" rel="nofollow">Astro</a>) offer scoped styling as an out-of-the-box feature. (Or you can use <a href="https://css-tricks.com/css-modules-part-1-need/" rel="nofollow">CSS modules</a>.)</p>
<p>Or, if what you like about Tailwind is the utility classes, there’s the much less intrusive <a href="https://open-props.style/" rel="nofollow">Open Props</a>.</p>
<p>Point is: I think Tailwind served the world of pre-2023 frontend development quite well. I don’t expect fans or organizations to move, but I <em>do</em> think we’re actively outgrowing the need for it right now. Most of the answers Tailwind provided weren’t otherwise readily available at the time, but they’re now becoming more and more just parts of the platform, and of the other tools we’re already using anyway.</p>
<h3 id="some-backstory"><a aria-hidden="true" tabindex="-1" href="#some-backstory"><span class="icon icon-link"></span></a>Some backstory</h3>
<p>There was a time in my career when I, along with the rest of the frontend engineers I worked with, were <em>forced</em> to write absolutely <em>everything</em> in Tailwind. There literally wasn’t a stylesheet to even put CSS into; it was forbidden. We couldn’t even edit the Tailwind config file for most of the time I was there. The powers at that company decided that was the best way to keep us all consistent. If it wasn’t Tailwind, it didn’t ship.</p>
<p>(<em>Everybody I’ve told this to finds it bafflingly ridiculous. Even the most ardent Tailwind fans cite the ability to simply break out of it and write CSS any time as a necessary and beneficial feature. Nonetheless, it was our reality.</em>)</p>
<p>Clearly, there was a sharp lack of representation in the room when that decision was made.</p>
<p>We, the frontend team, tried reasoning with the more senior and more backend-focused developers in charge of this tooling decision; tried to explain why an all-Tailwind, no-CSS approach was not just crippling our ability to execute on our designers’ creations, but forcing us into questionable architecture choices as well. (Custom components proliferated out of control, to the point that it almost didn’t make sense to have components at all.)</p>
<p>We also explained how this change was having adverse effects, both on the workers and the work. By putting our best tools just out of reach, we were forced to put more effort into crude workarounds, just to achieve mediocre results.</p>
<p>We weren’t happy, and neither were the designers whose work we were implementing. (<em>Try explaining to a designer that what they want would be not only possible, but relatively trivial, if you weren’t locked into a Tailwind-only world.</em>)</p>
<p>Besides, this just resulted in JIT styles being abused all over the place. The compiled stylesheet on the project had ballooned to unreasonable scale, because every team was just implementing their own one-offs willy-nilly.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>Both the process and the output suffered; things were harder to build, and they turned out worse.</p>
</aside>

<p class="callout svelte-rzjeng"></p>
<p>Both the process and the output suffered; things were harder to build, and they turned out worse.</p>

<p>Unfortunately, try as we did, those explanations were summarily dismissed. It was just like the Mario Kart conversation from earlier; <em>why could you possibly want to go off the track? You’re not supposed to do that!</em></p>
<p>To the powers in that company, Tailwind <em>was</em> the be-all, end-all solution. In their minds, anybody who was complaining just didn’t understand, or didn’t know what they were doing.</p>
<p>They couldn’t fathom the downsides.</p>
<p>In the months that followed, many of my colleagues transferred to other teams in other areas of the company. Some (like me) left entirely. That admittedly wasn’t my main reason, and I suspect it wasn’t anyone else’s either. But still: being creatively stifled by people who don’t try to understand your problems and don’t really trust that you even know what you’re talking about to begin with…well, it has a way of sticking with you.</p>
<p>I think often about what I could have said in those conversations; what might have made those more senior developers in charge of our projects understand the lose-lose situation they’d put us into; made them realize that where they saw a shining sunrise of pure upside, we saw long, dark shadows of disadvantages.</p>
<p>I don’t know if I could have. But since I’ve spent so much of this post talking about upsides and downsides, and how one person’s bug is another person’s feature, let’s close on that.</p>
<h3 id="the-two-sides-of-the-same-coin"><a aria-hidden="true" tabindex="-1" href="#the-two-sides-of-the-same-coin"><span class="icon icon-link"></span></a>The two sides of the same coin</h3>
<p>Where you might see a helpful copilot who keeps you on the track, I see a meddler who gets in the way at the worst possible moments.</p>
<p>Where Tailwind ostensibly saves you from context switching, it keeps me away from the places where I do my best work.</p>
<p>Where you see a tool that helps you get through a part of the job you either aren’t best at or just don’t enjoy, I see a missed opportunity for us to collaborate and put <em>both</em> our skillsets to optimal use. (I also see a cheapening of what I do; <em>your</em> code is considered important; <em>mine</em> is just something to be plowed through and done well enough to move on.)</p>
<p>Where you see a solved problem, I see tech debt that simply hasn’t come due yet; where you saw how easily and quickly you could write something the <em>first</em> time, I know from painful experience how difficult it will make future refactors and rewrites.</p>
<p>Where you see a tool that promises freedom, I see a lock-in mechanism that will be incredibly harrowing to undo.</p>
<p>Where you see easy prototyping and fast styling, I see a tool that’s railroading us into speed-running making all the same web pages and interfaces everybody is, too.</p>
<p>Where you see complexity simplified, I see a once-powerful tool, dulled and watered-down almost beyond recognition.</p>
<p>Where you see empowerment, I see suppression.</p>
<p>Where you see protective walls, I see a constricting cage.</p>
<p>We’re both just observing the same truths from different points of view.</p>
<p>We’re both wrong. We’re both right.</p>
<p>It just depends how we intend to navigate the course.</p>
</article>


<hr>

<footer>
<p>
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
</svg> Suivre</a> •
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
</svg> Pro</a> •
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
</svg> Email</a> •
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
</svg> Légal</abbr>
</p>
<template id="theme-selector">
<form>
<fieldset>
<legend><svg class="icon icon-brightness-contrast">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
</svg> Thème</legend>
<label>
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
</label>
<label>
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé
</label>
<label>
<input type="radio" value="light" name="chosen-color-scheme"> Clair
</label>
</fieldset>
</form>
</template>
</footer>
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
<script>
function loadThemeForm(templateName) {
const themeSelectorTemplate = document.querySelector(templateName)
const form = themeSelectorTemplate.content.firstElementChild
themeSelectorTemplate.replaceWith(form)

form.addEventListener('change', (e) => {
const chosenColorScheme = e.target.value
localStorage.setItem('theme', chosenColorScheme)
toggleTheme(chosenColorScheme)
})

const selectedTheme = localStorage.getItem('theme')
if (selectedTheme && selectedTheme !== 'undefined') {
form.querySelector(`[value="${selectedTheme}"]`).checked = true
}
}

const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
window.addEventListener('load', () => {
let hasDarkRules = false
for (const styleSheet of Array.from(document.styleSheets)) {
let mediaRules = []
for (const cssRule of styleSheet.cssRules) {
if (cssRule.type !== CSSRule.MEDIA_RULE) {
continue
}
// WARNING: Safari does not have/supports `conditionText`.
if (cssRule.conditionText) {
if (cssRule.conditionText !== prefersColorSchemeDark) {
continue
}
} else {
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
continue
}
}
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
}

// WARNING: do not try to insert a Rule to a styleSheet you are
// currently iterating on, otherwise the browser will be stuck
// in a infinite loop…
for (const mediaRule of mediaRules) {
styleSheet.insertRule(mediaRule.cssText)
hasDarkRules = true
}
}
if (hasDarkRules) {
loadThemeForm('#theme-selector')
}
})
</script>
</body>
</html>

+ 161
- 0
cache/2023/2074a4d527220f5ddf2dc0b4e678c83a/index.md 查看文件

@@ -0,0 +1,161 @@
title: Classic rock, Mario Kart, and why we can’t agree on Tailwind
url: https://joshcollinsworth.com/blog/tailwind-is-smart-steering
hash_url: 2074a4d527220f5ddf2dc0b4e678c83a

<p>When my brother and I were younger, our tastes in music were nearly polar opposites. I was a total hipster, and valued what I saw as artistic integrity and creative innovation. I was almost entirely into new and active, of-the-moment artists.</p>
<p>In contrast, my brother made virtually no judgments at all. He was open to anything and everything, but his mainstays centered on classic rock of the ’70s and ’80s. He loved rediscovering decades-old albums by many of the most well-known artists of their time.</p>
<p>Like any good elitist snob, I hated anything popular. So naturally, my brother’s kind of music was anathema to me at the time.</p>
<p>My brother never really understood what it was I seemed to dislike so strongly. So finally, one day, he asked me.</p>
<p>I thought about the question for a minute, and then summarized: “it’s all style and no substance.”</p>
<p>He burst out laughing.</p>
<p>“That’s the <em>best</em> thing about it!” he said with a huge grin. “That’s the whole point!”</p>
<p>I knew we looked at the same things in opposite ways. But I had never realized before then that, wildly enough, we had <em>all the exact same reasons</em> for it.</p>
<h2 id="we-dont-disagree-where-we-think-we-do"><a aria-hidden="true" tabindex="-1" href="#we-dont-disagree-where-we-think-we-do"><span class="icon icon-link"></span></a>We don’t disagree where we think we do</h2>
<p>Tailwind is nearly as ubiquitous as it is polarizing these days. (I trust if you’ve been interested enough to read this far, I don’t need to cite any sources there.)</p>
<p>Proponents show a near cult-like devotion to Tailwind, some even going so far as to claim it’s “fixed” CSS—or at the very least, made it manageable and predictable in a way it wasn’t for them before. Most frontend frameworks and products feature a first-class Tailwind integration, due to its soaring popularity.</p>
<p>Detractors, on the other hand, claim Tailwind is messy; it gets in the way; it violates core fundamentals of web development; it cuts against the grain; it diminishes the power of CSS; and/or that it does the <em>opposite</em> of what it’s supposed to, adding complexity and making projects even <em>harder</em> to maintain.</p>
<p>Many who use Tailwind never want to go back; many who <em>don’t</em> never want to.</p>
<p>How could we possibly disagree so sharply?</p>
<p>After using Tailwind for a good while now—both professionally and on a few small personal projects—I’ve come to what might just be the most unpopular opinion of all, in regards to that question:</p>
<p><strong>Both sides are right.</strong></p>
<p>…Ok, ok, I can hear the boos. Nobody likes a centrist. But hear me out.</p>
<p>We won’t ever get anywhere debating the merits or flaws of Tailwind, for the same reason my brother and I never persuaded each other on music.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>We could’ve dissected Bon Jovi and Sufjan Stevens all we wanted. But it didn’t matter, because our disagreement ultimately started before either one of us ever pressed the play button.</p>
</aside>

<p class="callout svelte-rzjeng"></p><p>We could’ve dissected Bon Jovi and Sufjan Stevens all we wanted. But it didn’t matter, because our disagreement ultimately started before either one of us ever pressed the play button.</p>

<p>It turns out, he and I actually saw every artist and album in pretty much exactly the same way to begin with.</p>
<p>We just never agreed on what was a feature, and what was a bug.</p>
<p>Likewise, I suspect most people on opposing sides of the Tailwind debate actually complete agree on Tailwind itself. I don’t think we disagree on atomic CSS, or utility classes; I think our contention comes from the valuations we made long before we ever chose our tools. Where one of us sees a selling point, the other sees a flaw.</p>
<p>Tailwind <em>is</em> great.</p>
<p>Tailwind is <em>also</em> a bad idea.</p>
<p>And it is both, at the same time, for exactly the same reasons.</p>

<p>If you’ve played <a href="https://www.nintendo.com/store/products/mario-kart-8-deluxe-switch/" rel="nofollow">Mario Kart 8 Deluxe</a> on Switch, you might know the game offers a feature called <a href="https://mariokart.fandom.com/wiki/Smart_Steering" rel="nofollow">Smart Steering</a>.</p>
<p>(<em>Yes, this is still about Tailwind. Bear with me here</em>.)</p>
<p>Smart Steering is essentially an AI copilot. When enabled, you control your racer as normal, except that if you start to go off the road, the game will automatically take the wheel and steer you back on course. No more slogging through the grass, or plummeting off the side of the track.</p>
<p>Smart Steering can help even the playing field among any mixed group (particularly on higher engine classes, where even very good players may find themselves hurling off the track).</p>
<p>But as you get better and better at the game, Smart Steering begins to help less and less…until eventually, it starts getting in the way instead.</p>
<p><img src="/images/post_images/impact.png" alt="A chart demonstrating the above paragraph; as skill level increases, the impact of usage goes from entirely positive to entirely negative."></p>
<p>Smart Steering will prevent you from taking shortcuts, for one thing; it can’t tell <em>why</em> you’re going off the main road, but it jumps in the way and prevents you from doing so regardless. It may even intervene unexpectedly, pushing you off your intended course at inopportune moments.</p>
<p>You might have a strategic reason in mind for going off the road (maybe you were avoiding a ruinous obstacle, for example), but Smart Steering won’t allow it regardless.</p>
<p>Plus, as a balancing feature, the game’s strongest speed boosts are disabled for players utilizing Smart Steering.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>The more skilled you are, the more this feature designed to augment your abilities instead begins to inhibit them. But whether it makes you better or worse—and to what degree—will depend entirely on where you’re coming from.</p>
</aside>

<p class="callout svelte-rzjeng"></p><p>The more skilled you are, the more this feature designed to augment your abilities instead begins to inhibit them. But whether it makes you better or worse—and to what degree—will depend entirely on where you’re coming from.</p>

<p>I’m a very good Mario Kart player (<em>I spent some thirty years of my life playing Mario Kart games before Smart Steering came along, after all</em>), and so I can’t stand Smart Steering. I never use it. I might not notice it a lot of the time—it might even occasionally help me—but what stands out to me are the times it jumps in my way at the worst possible moments.</p>
<p>Smart Steering might keep me on the track, but it <em>also</em> suppresses my fullest abilities. It thwarts the best outcomes just as commonly as it averts the worst disasters.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>To those sufficiently skilled with CSS, Tailwind feels like being forced to code with Smart Steering on.</p>
</aside>

<p class="callout svelte-rzjeng"></p><p>To those sufficiently skilled with CSS, Tailwind feels like being forced to code with Smart Steering on.</p>

<p>That’s why any conversation I have with somebody who likes Tailwind will probably be like trying to explain why I don’t like Smart Steering to a Mario Kart player who relies on it.</p>
<blockquote><p>“I don’t like that it keeps me from driving off the track.”</p>
<p>“What!? Why would you ever want to do that!?”</p>
<p>“You can actually skip ahead if you use this trick right here…”</p>
<p>“Ugh, what a hacky workaround. It’s really best practice to stay on the track, you know.”</p>
<p>“Look, I understand why you’re saying that, but I’ve been doing this for a long time, and I know what I’m doing.”</p>
<p>“I just don’t know why you’d make things harder on yourself, only to do something you’re not supposed to do anyway.”</p></blockquote>
<div class="side-note svelte-1j8wjk4"><p>I realize I might sound rather arrogant in choosing this comparison. Mario Kart is a racing game, after all, which might seem to imply that I think of myself as a winner, and people who don’t do things my way as inferior.</p>
<p>That’s not at all my intent; I only mean to point out that if Tailwind is your thing, it’s probably because we’re coming from different places, and like to play the game differently, for our own very valid reasons.</p>
</div>
<h2 id="there-are-no-benefits-without-tradeoffs"><a aria-hidden="true" tabindex="-1" href="#there-are-no-benefits-without-tradeoffs"><span class="icon icon-link"></span></a>There are no benefits without tradeoffs</h2>
<p>For the most part, it’s perfectly fine if we see the same thing in two different, even opposite ways. However, there’s one universal truth I think it’s important we agree on as common ground, namely: <em>benefits always come with tradeoffs</em> (and vice versa).</p>
<p>When it comes to frameworks, at least (CSS or otherwise), any place things seem simpler, we’re actually just experiencing the <em>upside</em> of some architectural decision. That decision, whatever it was, has downsides elsewhere; some other part of the chain, further away from our view, is now also <em>more</em> complex or difficult than it used to be.</p>
<p>Don’t get me wrong; that’s still usually a good thing. The new set of tradeoffs is often preferable to the old set; we wouldn’t use frameworks if that weren’t the case.</p>
<p>But complexity always exists, even if it’s remanded to a place you don’t regularly experience it.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>Problems in tech don’t <em>vanish</em>; they simply get reconfigured into new shapes, with new pointy edges in new places.</p>
</aside>

<p class="callout svelte-rzjeng"></p><p>Problems in tech don’t <em>vanish</em>; they simply get reconfigured into new shapes, with new pointy edges in new places.</p>

<p>So we should be incredibly skeptical of any framework (any <em>product</em>, really) that claims to have eliminated complexity—destroyed it, turned it into nothing—without being honest about the byproducts.</p>
<p>Ok, fine. Why is this all important?</p>
<p>Because Tailwind is often marketed (and spoken of by many of its proponents) as a framework that’s achieved this impossible task of obliterating CSS’s complexity from existence.</p>
<p>If you would make this argument, I would firmly dissent. But I would also agree you’re probably right <em>in your case</em>.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>You likely don’t work in the areas where the tradeoffs are, so they might seem like they don’t exist to you.</p>
</aside>

<p class="callout svelte-rzjeng"></p><p>You likely don’t work in the areas where the tradeoffs are, so they might seem like they don’t exist to you.</p>

<p>Boring and lazily centrist as it may sound: I think we have to agree that Tailwind and vanilla CSS are both unique sets of tradeoffs. Whether we consider one better than the other will depend very much on where we spend our time, what we value, and what we ultimately consider to be <em>the point</em> of all of this.</p>
<p>Let’s be fair: CSS <em>does</em> have myriad complexities and pitfalls, and Tailwind admittedly smooths out many of those rough edges, in various ways.</p>
<p>But CSS is <em>also</em> an incredibly powerful programming language, which Tailwind diminishes, creating liabilities of its own along the way.</p>
<p>The only <em>invalid</em> point of view is that you can have all benefits and no downsides. It just doesn’t work that way.</p>
<p><em>It all depends™.</em></p>
<h2 id="the-divide-where-do-you-want-to-be"><a aria-hidden="true" tabindex="-1" href="#the-divide-where-do-you-want-to-be"><span class="icon icon-link"></span></a>The divide: where do you want to be?</h2>
<p>So if what we actually disagree on is what we value, that naturally begs the question: where does this divide come from?</p>
<p>To me, the gap between Tailwind supporters and critics comes down to what part of the job they consider to be the most important, or perhaps, which part they just prefer to focus on.</p>
<p>Because continuing to come up with synonyms for supporters and detractors is tedious, for the sake of this section, I propose we give the opposing groups names. These names will, in themselves, be coarse generalizations, which means they will have obvious outliers and glaring contradictions. But for the sake of a model that is hopefully useful despite its flaws:</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>Let’s call the generally pro-Tailwind group <em>Builders</em>, and let’s call the generally anti-Tailwind group <em>Crafters</em>.</p>
</aside>

<p class="callout svelte-rzjeng"></p><p>Let’s call the generally pro-Tailwind group <em>Builders</em>, and let’s call the generally anti-Tailwind group <em>Crafters</em>.</p>

<p>This isn’t to say that Crafters don’t build things, or that the Builders aren’t skilled craftspeople. But as a quick and messy shorthand, let’s go with it for a moment, because I think it hints at the values of these two groups.</p>
<h3 id="builders"><a aria-hidden="true" tabindex="-1" href="#builders"><span class="icon icon-link"></span></a>Builders</h3>
<p>Builders clearly value getting the work done as quickly and efficiently as possible. They are making something—likely something with parts beyond the frontend—and are eager to see it through to completion.</p>
<p>Builders tend to tout Tailwind as the saving grace that got rid of the hard parts of CSS, after all. They like that Tailwind makes the work tidy and fast.</p>
<p>That, in turn, strongly implies Builders value getting through this particular part of their work as quickly and easily as possible. (We all value efficient productivity, of course. But <em>where</em> you value it may implicitly say something about your priorities.)</p>
<p>This also implies one or both of the following: either a) that they found this work overly challenging before; and/or b) that this is not the part of their job they wish to be challenged in. They likely don’t shy away from challenge; they probably just prefer it in different form.</p>
<p>Again: exceptions and outliers will be plentiful, of course. But: as a group, Builders tend to be people who’ve spent their careers, if not in other parts of the stack, then at least in other areas of frontend. That is: for most Builders (though not all), CSS is not a specialty—or at any rate, not a priority. Many people who like Tailwind are also very good at CSS, but those Builders tend to be bringing a more balanced approach, where they use Tailwind for the broad strokes of utility classes, and heavily customize the config file and/or write their own CSS to fill in the gaps.</p>
<h3 id="crafters"><a aria-hidden="true" tabindex="-1" href="#crafters"><span class="icon icon-link"></span></a>Crafters</h3>
<p>On the other side, the Crafters tend to be seasoned CSS specialists, and almost always enjoy the part of the work that Tailwind is supposed to make easier. It’s fair to say they’ve overcome the challenge presented by CSS—or, at least, that this is where they <em>like</em> to be challenged.</p>
<p>Crafters may be building holistic products and projects, just like Builders. But Crafters generally are less focused on <em>getting through</em> the frontend as a <em>part</em> of that work, and instead see the frontend as <em>the product itself</em>.</p>
<p>Because of their skills with (or willingness to be challenged by) CSS, Crafters often see Tailwind as a blunt instrument that dampens their abilities. At worst, Tailwind locks off the best parts of both CSS and of their jobs. (Tailwind can’t keep up with new CSS features, and even where it can, it can’t implement everything or do everything that CSS can.) At best, it represents a hefty learning curve, just to get back to where they already are.</p>
<p>Speed isn’t an issue for Crafters—or at least, not a priority; they are more concerned with the handiwork of their product. Styling is exactly where they want to be, because they value doing the work uniquely well over doing it well enough. Again, they see this work as central and defining to the product, and not just a detail of it.</p>
<h3 id="builders-and-crafters"><a aria-hidden="true" tabindex="-1" href="#builders-and-crafters"><span class="icon icon-link"></span></a>Builders and Crafters</h3>
<p>As <a href="https://adactio.com/journal/18982" rel="nofollow">Jeremy Keith put it so well</a>: where it comes to styling, Builders want imperative programming; they want to specify what they want, where they want, how they want it. No surprises.</p>
<p>Crafters instead want declarative programming; they understand how to wield the power of creating rules of governance within a complex system, and wish to use that power, rather than <a href="https://buildexcellentwebsit.es/" rel="nofollow">micromanaging the browser</a>.</p>
<p>Both Builders and Crafters have fully valid points of view. And in fairness, as I implied earlier, there exists a rich gradient of options between the two. You can balance workmanship and craftsmanship. You can use Tailwind only as much as you choose, and opt to reach for hand-authored CSS the rest of the time.</p>
<p>The question is really just what you value, and where you want to spend your time.</p>
<h2 id="conclusion-my-point-of-view-on-tailwind"><a aria-hidden="true" tabindex="-1" href="#conclusion-my-point-of-view-on-tailwind"><span class="icon icon-link"></span></a>Conclusion: my point of view on Tailwind</h2>
<p>To continue using the distinctions above: I consider myself a Crafter. I am, therefore, wary of any tool that nudges me towards building things a certain way—<em>especially</em> when it’s proliferated across my profession, and has become so popular you can tell most websites built with it at a glance.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>In my view, the more you optimize for building quickly, the more you optimize for homogeneity.</p>
</aside>

<p class="callout svelte-rzjeng"></p><p>In my view, the more you optimize for building quickly, the more you optimize for homogeneity.</p>

<p>I acknowledge that Tailwind might be a great solution, if its downsides are not issues for you, or if you have other means of mitigating them.</p>
<p>If it solves your problems, by all means, disregard my qualms as the ramblings of an old man yelling at a Tailwind-shaped cloud. It’s only a tool, after all. It shouldn’t be something we have to agree on if we’re not working together, let alone a pillar of anyone’s identity.</p>
<p>But to me, Tailwind <em>is</em> a problem in and of itself. The tradeoffs may be a net benefit for your use cases, and if so, that’s great. I’ve tested Tailwind pretty thoroughly, however, and I’ve concluded that it is <em>not</em> a net positive in my case. I don’t mind working with it on a team (it <em>is</em> useful as a unifying system, after all), but at the very least, I request the freedom to break out of it at my discretion as needed and as it’s useful. I feel <em>anyone</em> working with Tailwind should have this autonomy. I’d even go so far as to say you’re suppressing your Crafters if you <em>don’t</em> allow them that.</p>
<p>I do not care how fast I build, or how easily I prototype, so much as I care that I am building something uniquely good, and building it the right way.</p>
<p>Besides: I would argue that the last few years of growth in browser CSS, as well as frontend frameworks, have rendered Tailwind’s benefits largely moot for many use cases. We have <a href="https://fullystacked.net/posts/scope-in-css/" rel="nofollow"><code>@scope</code> in CSS now</a>. We have <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@layer" rel="nofollow">cascade layers</a>. Even if you don’t want to reach for those from the platform, most frontend frameworks (like <a href="https://kit.svelte.dev/" rel="nofollow">SvelteKit</a>, <a href="https://vuejs.org" rel="nofollow">Vue</a>, and <a href="https://astro.build" rel="nofollow">Astro</a>) offer scoped styling as an out-of-the-box feature. (Or you can use <a href="https://css-tricks.com/css-modules-part-1-need/" rel="nofollow">CSS modules</a>.)</p>
<p>Or, if what you like about Tailwind is the utility classes, there’s the much less intrusive <a href="https://open-props.style/" rel="nofollow">Open Props</a>.</p>
<p>Point is: I think Tailwind served the world of pre-2023 frontend development quite well. I don’t expect fans or organizations to move, but I <em>do</em> think we’re actively outgrowing the need for it right now. Most of the answers Tailwind provided weren’t otherwise readily available at the time, but they’re now becoming more and more just parts of the platform, and of the other tools we’re already using anyway.</p>
<h3 id="some-backstory"><a aria-hidden="true" tabindex="-1" href="#some-backstory"><span class="icon icon-link"></span></a>Some backstory</h3>
<p>There was a time in my career when I, along with the rest of the frontend engineers I worked with, were <em>forced</em> to write absolutely <em>everything</em> in Tailwind. There literally wasn’t a stylesheet to even put CSS into; it was forbidden. We couldn’t even edit the Tailwind config file for most of the time I was there. The powers at that company decided that was the best way to keep us all consistent. If it wasn’t Tailwind, it didn’t ship.</p>
<p>(<em>Everybody I’ve told this to finds it bafflingly ridiculous. Even the most ardent Tailwind fans cite the ability to simply break out of it and write CSS any time as a necessary and beneficial feature. Nonetheless, it was our reality.</em>)</p>
<p>Clearly, there was a sharp lack of representation in the room when that decision was made.</p>
<p>We, the frontend team, tried reasoning with the more senior and more backend-focused developers in charge of this tooling decision; tried to explain why an all-Tailwind, no-CSS approach was not just crippling our ability to execute on our designers’ creations, but forcing us into questionable architecture choices as well. (Custom components proliferated out of control, to the point that it almost didn’t make sense to have components at all.)</p>
<p>We also explained how this change was having adverse effects, both on the workers and the work. By putting our best tools just out of reach, we were forced to put more effort into crude workarounds, just to achieve mediocre results.</p>
<p>We weren’t happy, and neither were the designers whose work we were implementing. (<em>Try explaining to a designer that what they want would be not only possible, but relatively trivial, if you weren’t locked into a Tailwind-only world.</em>)</p>
<p>Besides, this just resulted in JIT styles being abused all over the place. The compiled stylesheet on the project had ballooned to unreasonable scale, because every team was just implementing their own one-offs willy-nilly.</p>
<aside class="pull-quote svelte-omo24t" aria-hidden="true" hidden><p>Both the process and the output suffered; things were harder to build, and they turned out worse.</p>
</aside>

<p class="callout svelte-rzjeng"></p><p>Both the process and the output suffered; things were harder to build, and they turned out worse.</p>

<p>Unfortunately, try as we did, those explanations were summarily dismissed. It was just like the Mario Kart conversation from earlier; <em>why could you possibly want to go off the track? You’re not supposed to do that!</em></p>
<p>To the powers in that company, Tailwind <em>was</em> the be-all, end-all solution. In their minds, anybody who was complaining just didn’t understand, or didn’t know what they were doing.</p>
<p>They couldn’t fathom the downsides.</p>
<p>In the months that followed, many of my colleagues transferred to other teams in other areas of the company. Some (like me) left entirely. That admittedly wasn’t my main reason, and I suspect it wasn’t anyone else’s either. But still: being creatively stifled by people who don’t try to understand your problems and don’t really trust that you even know what you’re talking about to begin with…well, it has a way of sticking with you.</p>
<p>I think often about what I could have said in those conversations; what might have made those more senior developers in charge of our projects understand the lose-lose situation they’d put us into; made them realize that where they saw a shining sunrise of pure upside, we saw long, dark shadows of disadvantages.</p>
<p>I don’t know if I could have. But since I’ve spent so much of this post talking about upsides and downsides, and how one person’s bug is another person’s feature, let’s close on that.</p>
<h3 id="the-two-sides-of-the-same-coin"><a aria-hidden="true" tabindex="-1" href="#the-two-sides-of-the-same-coin"><span class="icon icon-link"></span></a>The two sides of the same coin</h3>
<p>Where you might see a helpful copilot who keeps you on the track, I see a meddler who gets in the way at the worst possible moments.</p>
<p>Where Tailwind ostensibly saves you from context switching, it keeps me away from the places where I do my best work.</p>
<p>Where you see a tool that helps you get through a part of the job you either aren’t best at or just don’t enjoy, I see a missed opportunity for us to collaborate and put <em>both</em> our skillsets to optimal use. (I also see a cheapening of what I do; <em>your</em> code is considered important; <em>mine</em> is just something to be plowed through and done well enough to move on.)</p>
<p>Where you see a solved problem, I see tech debt that simply hasn’t come due yet; where you saw how easily and quickly you could write something the <em>first</em> time, I know from painful experience how difficult it will make future refactors and rewrites.</p>
<p>Where you see a tool that promises freedom, I see a lock-in mechanism that will be incredibly harrowing to undo.</p>
<p>Where you see easy prototyping and fast styling, I see a tool that’s railroading us into speed-running making all the same web pages and interfaces everybody is, too.</p>
<p>Where you see complexity simplified, I see a once-powerful tool, dulled and watered-down almost beyond recognition.</p>
<p>Where you see empowerment, I see suppression.</p>
<p>Where you see protective walls, I see a constricting cage.</p>
<p>We’re both just observing the same truths from different points of view.</p>
<p>We’re both wrong. We’re both right.</p>
<p>It just depends how we intend to navigate the course.</p>

+ 324
- 0
cache/2023/49f2ce04dd0beb94dc2f662163bc6339/index.html 查看文件

@@ -0,0 +1,324 @@
<!doctype html><!-- This is a valid HTML5 document. -->
<!-- Screen readers, SEO, extensions and so on. -->
<html lang="fr">
<!-- Has to be within the first 1024 bytes, hence before the `title` element
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
<meta charset="utf-8">
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
<!-- The viewport meta is quite crowded and we are responsible for that.
See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- Required to make a valid HTML5 document. -->
<title>Some notes on Local-First Development (archive) — David Larlet</title>
<meta name="description" content="Publication mise en cache pour en conserver une trace.">
<!-- That good ol' feed, subscribe :). -->
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
<!-- Generated from https://realfavicongenerator.net/ such a mess. -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
<link rel="manifest" href="/static/david/icons2/site.webmanifest">
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
<meta name="msapplication-TileColor" content="#f7f7f7">
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
<!-- Documented, feel free to shoot an email. -->
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
<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>
<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>
<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>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<script>
function toggleTheme(themeName) {
document.documentElement.classList.toggle(
'forced-dark',
themeName === 'dark'
)
document.documentElement.classList.toggle(
'forced-light',
themeName === 'light'
)
}
const selectedTheme = localStorage.getItem('theme')
if (selectedTheme !== 'undefined') {
toggleTheme(selectedTheme)
}
</script>

<meta name="robots" content="noindex, nofollow">
<meta content="origin-when-cross-origin" name="referrer">
<!-- Canonical URL for SEO purposes -->
<link rel="canonical" href="https://bricolage.io/some-notes-on-local-first-development/">

<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">


<article>
<header>
<h1>Some notes on Local-First Development</h1>
</header>
<nav>
<p class="center">
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="https://bricolage.io/some-notes-on-local-first-development/" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p>A few months ago in June, I attended <a href="https://www.youtube.com/results?search_query=Local-First+Meetup+Berlin+%231">a local-first meetup in Berlin</a> organized by Johannes Schickling, formerly the founder of Prisma. An intellectual crackle filled the air as we watched demos of new libraries and products. Many of us had been independently playing around with local-first development ideas for a while — in my case, over a decade — and the in-person meetup gave us the chance to trade notes late into the night.</p>
<p><span class="gatsby-resp-image-wrapper">
<a class="gatsby-resp-image-link" href="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/49ee4/berlin-meetup.jpg" target="_blank" rel="noopener">
<span class="gatsby-resp-image-background-image"></span>
<img class="gatsby-resp-image-image" alt="Picture of Berlin Local-First meetup" title="" src="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c72d/berlin-meetup.jpg" srcset="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/a80bd/berlin-meetup.jpg 148w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c91a/berlin-meetup.jpg 295w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c72d/berlin-meetup.jpg 590w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/a8a14/berlin-meetup.jpg 885w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/fbd2c/berlin-meetup.jpg 1180w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/49ee4/berlin-meetup.jpg 3630w" sizes="(max-width: 590px) 100vw, 590px" loading="lazy" decoding="async">
</a>
</span></p>
<p>In the months since, I’ve continued to tinker with these technologies and collected some point-in-time notes on significant developments and what might happen in the years to come.</p>
<h3 id="table-of-contents"><a href="#table-of-contents" aria-label="table of contents permalink" class="anchor before"></a>Table of Contents</h3>

<h2 id="whats-happening"><a href="#whats-happening" aria-label="whats happening permalink" class="anchor before"></a>What’s Happening?</h2>
<p>The web feels ready for a major upgrade. We had tightly coupled web frameworks in the Rails/Django years and lost them with the shift to API-powered SPAs. The developing database-grade sync technology will tightly recouple our application stacks allowing for a new era of framework innovation.</p>
<p>I see “local-first” as shifting reads and writes to an embedded database in each client via“sync engines” that facilitate data exchange between clients and servers. Applications like Figma and Linear pioneered this approach, but it’s becoming increasingly easy to do. The benefits are multiple:</p>
<ul>
<li>Simplified state management for developers</li>
<li>Built-in support for real-time sync, offline usage, and multiplayer collaborative features</li>
<li>Faster (60 FPS) CRUD</li>
<li>More robust applications for end-users</li>
</ul>
<p>(Some good reading material: this local-first case study on <a href="https://riffle.systems/essays/prelude/">building reactive, data-centric apps</a> and <a href="https://www.youtube.com/watch?v=jxuXGeMJsBU&amp;t=1s">Johannes’ talk in Berlin</a>; I will include some more links later.)</p>
<p>Like the shift to componentized JavaScript UI over the last decade, I believe local-first will be the next large paradigm shift for rich client apps and work its way through the application world over the next decade</p>
<h2 id="why-is-local-first-happening-now"><a href="#why-is-local-first-happening-now" aria-label="why is local first happening now permalink" class="anchor before"></a>Why is Local-First Happening Now?</h2>
<p>I’ve read about and played with local-first type ideas for a decade or so but only now does it seem to be gaining steam with many young startups rushing to productize its ideas.</p>
<p>Figma, Superhuman, and Linear are good examples of pioneering startups in the local-first paradigm and all rely on local-first ideas and bespoke sync engines to support their speed and multiplayer UX.</p>
<p>Many builders now see local-first as a key way to differentiate their applications.</p>
<p>Why is this happening now?</p>
<p>My general model for change in technology is that a given community of practice (like application developers) can only adopt one large paradigm shift at a time. While local-first ideas have been floating around for decades, practitioners have so far been focused on more fundamental changes.</p>
<p>What we’ve seen over the last decade is that application speed and collaboration features are powerful vectors to shake up an incumbent industry. Figma used local-first to displace Sketch and InVision; Linear is using local-first to displace Jira.</p>
<p>We’ve shifted from Rails-type server-rendered apps, to single-page-apps powered by APIs. A core lesson from this transition is that while standard REST and GraphQL APIs are very easy to get started with for solving client/server sync, they require significant effort and skill to scale and refine and they struggle with use-cases like multiplayer and offline support.</p>
<p>Sync engines are a robust database-grade syncing technology to ensure that data is consistent and up-to-date. It’s an ecosystem-wide refactor that many talented groups are exploring to attempt to simplify the application stack.</p>
<p>Assuming they succeed, they’ll provide a solid substrate for new types of framework that can rely on local data and rock solid sync.</p>
<h2 id="will-most-rich-client-apps-use-local-first"><a href="#will-most-rich-client-apps-use-local-first" aria-label="will most rich client apps use local first permalink" class="anchor before"></a>Will Most Rich Client Apps Use Local-First?</h2>
<p>I’ve chatted with a number of developers who — frustrated at maintaining the homegrown bespoke sync systems they wrote — are replacing it with new local-first tooling. The tooling feels a lot better and having standard primitives make it easier to build great experiences.</p>
<p>Local-first is developing into an ecosystem similar to authentication services but for handling data and features that need to be real-time, collaborative, or offline.</p>
<p>The key question for us technologists is whether local-first will remain a niche technology or if it’ll gradually replace the current API-based approach.</p>
<p>It’s still early but I’m confident that at a minimum, we’ll see multiple breakout startups along with a few healthy open-source ecosystems around the different approaches. And if local-first becomes the new default paradigm for handling data,, it will be much larger and reshape many parts of the cloud ecosystem.</p>
<p>But: there are many issues to solve first! Let’s look in detail at one of the first issues that people encounter: handling CRUD operations.</p>
<h3 id="crud-with-crdt-based-sync-engines"><a href="#crud-with-crdt-based-sync-engines" aria-label="crud with crdt based sync engines permalink" class="anchor before"></a>CRUD with CRDT-based Sync Engines</h3>
<p>To fully replace client-server APIs, sync engines need robust support for fine-grained access control and complex write validation.</p>
<p>The most basic use case for an API is <em>state transfer</em> from the server to the client. The client wants to show information about an object so reads the necessary data through the API.</p>
<p>Local-first tools all handle this perfectly. They ensure the latest object data is synced correctly to the client db for querying from the UI.</p>
<p>But while reads are generally easy, support for complex writes is still immature in local-first tooling. Clients tend to have unrestricted write access and updates are immediately synced to other clients. While this is generally fine for text collaboration or multiplayer drawing, this wouldn’t work for a typical ecommerce or SaaS application.</p>
<p>Local-first tools need somewhere to put arbitrary business logic written in code, because real-world systems start out elegant and simple and, over time, accumulate lots of messy, important chunks of logic.</p>
<p>A data property might normally be limited to 20 but Acme Corp paid $50k to get a limit of 100. Or write access needs to be restricted for sensitive information like account balances. Or writes need validation by third party APIs, like a calendar booking system that needs to ask an external calendar API if a time slot is open.</p>
<p>Normally, these inevitable chunks of code and logic live on the server behind an API. And we still need somewhere to put them in a local-first world.</p>
<p>Put differently: local-first CRDT-based sync engines can drive consistency within a system but real-world systems also need an authoritative server which can enforce consistency within external constraints and systems.</p>
<p>Or as Aaron Boodman, a <a href="https://replicache.dev/">Replicache</a> co-founder, put it: “CRDTs converge, but to where?”</p>
<h3 id="using-a-distributed-state-machine-to-handle-complex-writes"><a href="#using-a-distributed-state-machine-to-handle-complex-writes" aria-label="using a distributed state machine to handle complex writes permalink" class="anchor before"></a>Using a Distributed State Machine to Handle Complex Writes</h3>
<p>I asked Anselm Eickhoff and James Arthur (founders of <a href="https://jazz.tools/">Jazz</a> and <a href="https://electric-sql.com/">ElectricSQL</a> respectively, both CRDT-based tools) about how they suggest handling writes that need an authoritative server.</p>
<p>They both suggested emulating API request/response patterns through a distributed state machine running on a replicated object.</p>
<p>So let’s say a client wants to update the user name. It creates the following object:</p>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"machine"</span><span class="token operator">:</span> <span class="token string">"updateUserName"</span><span class="token punctuation">,</span>
<span class="token property">"state"</span><span class="token operator">:</span> <span class="token string">"requestedUpdate"</span><span class="token punctuation">,</span>
<span class="token property">"request"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
<span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span><span class="token punctuation">,</span>
<span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122496</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"response"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
<span class="token punctuation">}</span>


<span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Bob"</span><span class="token punctuation">,</span>
<span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span>
<span class="token punctuation">}</span></code></pre></div>
<p>The server listens for writes and upon receiving this one, validates the request and updates the state machine object along with the name on the user object (which again are synced back to clients as the “server” is just another client):</p>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"machine"</span><span class="token operator">:</span> <span class="token string">"updateUserName"</span><span class="token punctuation">,</span>
<span class="token property">"state"</span><span class="token operator">:</span> <span class="token string">"finished"</span><span class="token punctuation">,</span>
<span class="token property">"request"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
<span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span><span class="token punctuation">,</span>
<span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122496</span>
<span class="token punctuation">}</span>
<span class="token property">"response"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"error"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122996</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>


<span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
<span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span>
<span class="token punctuation">}</span></code></pre></div>
<p>This has the interesting and useful implication that in-flight requests are synced to other clients and it’s trivial for the server to emit progress updates. E.g. an app that supports a team of users uploading images for some nifty AI enhancements. The client can emit progress updates on the upload and the server as it works through enhancements.</p>
<p>In other words, requests/responses have the same multiplayer, offline, real-time sync properties as the rest of the app.</p>
<p>This pattern can be wrapped up in a standard mutation function e.g. <code class="language-text">const {res, error} = await updateName({ name: "fred" })</code></p>
<p>Jazz also lets you restrict writes to certain object fields to a specific role. Sensitive information would be marked read-only for clients who would need to request the server to update them.</p>
<h3 id="local-first-dx-is-still-being-explored"><a href="#local-first-dx-is-still-being-explored" aria-label="local first dx is still being explored permalink" class="anchor before"></a>Local-First DX is Still Being Explored</h3>
<p>Beyond the question of <em>can</em> you build any application with local-first tools, there’s still the question of whether devs will <em>want</em> to.</p>
<ul>
<li>Do advantages make it worth learning a new stack and migrating applications?</li>
<li>There’s a lot of missing components still — what will a full production DX for a local-first toolchain look like?</li>
<li>How complex will it feel to build a simple app?</li>
<li>Is there enough demand to fund all the new libraries and products that’ll need to be built?</li>
</ul>
<p>The success of Figma, Superhuman, and Linear suggest these issues will get solved in time.</p>
<h2 id="what-approaches-are-people-exploring-now"><a href="#what-approaches-are-people-exploring-now" aria-label="what approaches are people exploring now permalink" class="anchor before"></a>What Approaches are People Exploring Now?</h2>
<p>I’m grouping approaches I see into three broad categories.</p>
<h3 id="1-replicated-data-structures"><a href="#1-replicated-data-structures" aria-label="1 replicated data structures permalink" class="anchor before"></a>1. Replicated Data Structures</h3>
<p>These projects provide support for replicated data structures. They are convenient building blocks for any sort of real-time or multiplayer project. They typically give you APIs similar to native Javascript maps and arrays but which guarantee state updates are replicated to other clients and to the server.</p>
<p>It feels like magic when you can build a simple application and and see changes instantly replicate between devices with no additional work.</p>
<p>Most replicated data structures rely on CRDT algorithms to merge concurrent and offline edits from multiple clients.</p>
<p>There’s a number of open source and hosted projects offering replicated data structures, including the granddaddy in this space, Firebase, plus many newer ones.</p>
<p>These services are great for making parts of an app real-time / multiplayer. E.g. a drawing surface, a chat room, a notification system, presence, etc. They’re very simple to get started with and the shared data structures approach offers a much better DX than manually passing events through with websockets or push messaging services.</p>
<p>Open source projects:</p>

<p>Hosted services:</p>

<h3 id="2-replicated-database-tables"><a href="#2-replicated-database-tables" aria-label="2 replicated database tables permalink" class="anchor before"></a>2. Replicated Database Tables</h3>
<p>An approach several projects are taking is to sync from Postgres to a client db (generally SQLite). You pick tables (or materialized views) to sync to the client and then they get loaded along with real-time updates as writes land in Postgres.</p>
<p>SQLite in the browser is one big advantage of this approach as it gives you the rich, familiar querying power of SQL in the client.</p>
<p>Given Postgres’ widespread usage and central position in most application architectures, this is a great way to start with local-first. Instead of syncing data in and out of replicated data structures, you can read and write directly to Postgres as normal, confident that clients will be in sync.</p>
<p>I’ve built a number of job queues and notification systems over the years and they’ve all struggled with their version of the Byzantine Generals problem. Clients would miss an update (usually due to being offline), and then users would complain about zombie jobs that never finished. In contrast, replicated database tables mean the background process can simply write updates to Postgres, confident all connected clients will get the update.</p>
<h4 id="postgres-to-sqlite"><a href="#postgres-to-sqlite" aria-label="postgres to sqlite permalink" class="anchor before"></a>Postgres to SQLite:</h4>

<p>ElectricSQL and Powersync support syncing client writes back to Postgres and other clients.</p>

<ul>
<li>
<p>SQLite to SQLite</p>

</li>
<li>
<p>MongoDB to Client DB</p>

</li>
</ul>
<h3 id="3-replication-as-a-protocol"><a href="#3-replication-as-a-protocol" aria-label="3 replication as a protocol permalink" class="anchor before"></a>3. Replication as a Protocol</h3>
<p>The startup <a href="https://replicache.dev/">Replicache</a> has a unique “replication as a protocol” approach. Replicache is a client JS library along with a replication protocol — which lets you integrate arbitrary backends, provided you follow the spec. It’s more upfront work, as the sync engine is “some assembly required”, but as Replicache is mostly your own code, it gives the most flexibility and power of any local-first tool I’ve seen to date. The startup behind Replicache is also building Reflect, a hosted backend for Replicache.</p>
<h2 id="soshould-you-go-local-first"><a href="#soshould-you-go-local-first" aria-label="soshould you go local first permalink" class="anchor before"></a>So…Should You Go Local-First?</h2>
<p>I think it depends on your use case and risk tolerance.</p>
<p>For the right people and teams, it’s an exciting time to jump in. Realtime, multiplayer, and offline features will significantly improve much of our day-to-day software.</p>
<p>For almost any <strong>real-time use case</strong>, I’d choose <em>replicated data structures</em> over raw web sockets as they give you a much simpler DX and robust guarantees that clients will get updates.</p>
<p>For <strong>multiplayer and offline</strong>, again you’ll almost certainly want to pick an open source <em>replicated data structure</em> or hosted service. There’s a lot of difficult problems that they help solve.</p>
<p>The <em>Replicated Database</em> approach also works for the real-time, multiplayer, offline use cases. It should be especially useful for data-heavy applications and ones with many background processing writing into Postgres.</p>
<p>But in general, I’d still be wary of using local-first outside real-time / multiplayer / offline use cases. Local-first is definitely still bleeding-edge. You will hit unexpected problems. A good community has rapidly developed, but there’ll still be some stretches on the road where you’ll have to solve novel problems.</p>
<p>So: if you need local-first, see if it makes sense to isolate the local-first parts and architect the rest of the app (for now) in a more conventional fashion.</p>
<p>I’ve found that most of the major tools have plenty of examples and demos to play with and active Discord channels. There’s also a general local-first community over at <a href="https://localfirstweb.dev/">https://localfirstweb.dev/</a> and they hold regular meetups.</p>
<p>Following along, I’m getting a lot of the same vibes that I got in the then-nascent React community circa 2014 or 2015.</p>
<p>There are also a lot of interesting implications of local-first development in the realm of privacy, decentralization, data control and so on, but I’ll leave it to others more well-versed in these topics to flesh them out.</p>
<p>And some more links below. Happy building!</p>

<p>Discuss this post on <a href="">X.com née Twitter</a>, <a href="https://warpcast.com/kam/0x0c1632">Farcaster</a>, or <a href="https://bsky.app/profile/kam.bsky.social/post/3k77qihvoip2h">Bluesky</a></p>
<p><em>Thanks to Sam Bhagwat, Shannon Soper, Johannes Schickling, Andreas Klinger, Pekka Enberg, Anselm Eickhoff, and James Arthur for reading drafts of this post</em></p>
</article>


<hr>

<footer>
<p>
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
</svg> Suivre</a> •
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
</svg> Pro</a> •
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
</svg> Email</a> •
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
</svg> Légal</abbr>
</p>
<template id="theme-selector">
<form>
<fieldset>
<legend><svg class="icon icon-brightness-contrast">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
</svg> Thème</legend>
<label>
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
</label>
<label>
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé
</label>
<label>
<input type="radio" value="light" name="chosen-color-scheme"> Clair
</label>
</fieldset>
</form>
</template>
</footer>
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
<script>
function loadThemeForm(templateName) {
const themeSelectorTemplate = document.querySelector(templateName)
const form = themeSelectorTemplate.content.firstElementChild
themeSelectorTemplate.replaceWith(form)

form.addEventListener('change', (e) => {
const chosenColorScheme = e.target.value
localStorage.setItem('theme', chosenColorScheme)
toggleTheme(chosenColorScheme)
})

const selectedTheme = localStorage.getItem('theme')
if (selectedTheme && selectedTheme !== 'undefined') {
form.querySelector(`[value="${selectedTheme}"]`).checked = true
}
}

const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
window.addEventListener('load', () => {
let hasDarkRules = false
for (const styleSheet of Array.from(document.styleSheets)) {
let mediaRules = []
for (const cssRule of styleSheet.cssRules) {
if (cssRule.type !== CSSRule.MEDIA_RULE) {
continue
}
// WARNING: Safari does not have/supports `conditionText`.
if (cssRule.conditionText) {
if (cssRule.conditionText !== prefersColorSchemeDark) {
continue
}
} else {
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
continue
}
}
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
}

// WARNING: do not try to insert a Rule to a styleSheet you are
// currently iterating on, otherwise the browser will be stuck
// in a infinite loop…
for (const mediaRule of mediaRules) {
styleSheet.insertRule(mediaRule.cssText)
hasDarkRules = true
}
}
if (hasDarkRules) {
loadThemeForm('#theme-selector')
}
})
</script>
</body>
</html>

+ 157
- 0
cache/2023/49f2ce04dd0beb94dc2f662163bc6339/index.md 查看文件

@@ -0,0 +1,157 @@
title: Some notes on Local-First Development
url: https://bricolage.io/some-notes-on-local-first-development/
hash_url: 49f2ce04dd0beb94dc2f662163bc6339

<p>A few months ago in June, I attended <a href="https://www.youtube.com/results?search_query=Local-First+Meetup+Berlin+%231">a local-first meetup in Berlin</a> organized by Johannes Schickling, formerly the founder of Prisma. An intellectual crackle filled the air as we watched demos of new libraries and products. Many of us had been independently playing around with local-first development ideas for a while — in my case, over a decade — and the in-person meetup gave us the chance to trade notes late into the night.</p>
<p><span class="gatsby-resp-image-wrapper">
<a class="gatsby-resp-image-link" href="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/49ee4/berlin-meetup.jpg" target="_blank" rel="noopener">
<span class="gatsby-resp-image-background-image"></span>
<img class="gatsby-resp-image-image" alt="Picture of Berlin Local-First meetup" title="" src="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c72d/berlin-meetup.jpg" srcset="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/a80bd/berlin-meetup.jpg 148w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c91a/berlin-meetup.jpg 295w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c72d/berlin-meetup.jpg 590w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/a8a14/berlin-meetup.jpg 885w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/fbd2c/berlin-meetup.jpg 1180w,
https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/49ee4/berlin-meetup.jpg 3630w" sizes="(max-width: 590px) 100vw, 590px" loading="lazy" decoding="async">
</a>
</span></p>
<p>In the months since, I’ve continued to tinker with these technologies and collected some point-in-time notes on significant developments and what might happen in the years to come.</p>
<h3 id="table-of-contents"><a href="#table-of-contents" aria-label="table of contents permalink" class="anchor before"></a>Table of Contents</h3>

<h2 id="whats-happening"><a href="#whats-happening" aria-label="whats happening permalink" class="anchor before"></a>What’s Happening?</h2>
<p>The web feels ready for a major upgrade. We had tightly coupled web frameworks in the Rails/Django years and lost them with the shift to API-powered SPAs. The developing database-grade sync technology will tightly recouple our application stacks allowing for a new era of framework innovation.</p>
<p>I see “local-first” as shifting reads and writes to an embedded database in each client via“sync engines” that facilitate data exchange between clients and servers. Applications like Figma and Linear pioneered this approach, but it’s becoming increasingly easy to do. The benefits are multiple:</p>
<ul>
<li>Simplified state management for developers</li>
<li>Built-in support for real-time sync, offline usage, and multiplayer collaborative features</li>
<li>Faster (60 FPS) CRUD</li>
<li>More robust applications for end-users</li>
</ul>
<p>(Some good reading material: this local-first case study on <a href="https://riffle.systems/essays/prelude/">building reactive, data-centric apps</a> and <a href="https://www.youtube.com/watch?v=jxuXGeMJsBU&amp;t=1s">Johannes’ talk in Berlin</a>; I will include some more links later.)</p>
<p>Like the shift to componentized JavaScript UI over the last decade, I believe local-first will be the next large paradigm shift for rich client apps and work its way through the application world over the next decade</p>
<h2 id="why-is-local-first-happening-now"><a href="#why-is-local-first-happening-now" aria-label="why is local first happening now permalink" class="anchor before"></a>Why is Local-First Happening Now?</h2>
<p>I’ve read about and played with local-first type ideas for a decade or so but only now does it seem to be gaining steam with many young startups rushing to productize its ideas.</p>
<p>Figma, Superhuman, and Linear are good examples of pioneering startups in the local-first paradigm and all rely on local-first ideas and bespoke sync engines to support their speed and multiplayer UX.</p>
<p>Many builders now see local-first as a key way to differentiate their applications.</p>
<p>Why is this happening now?</p>
<p>My general model for change in technology is that a given community of practice (like application developers) can only adopt one large paradigm shift at a time. While local-first ideas have been floating around for decades, practitioners have so far been focused on more fundamental changes.</p>
<p>What we’ve seen over the last decade is that application speed and collaboration features are powerful vectors to shake up an incumbent industry. Figma used local-first to displace Sketch and InVision; Linear is using local-first to displace Jira.</p>
<p>We’ve shifted from Rails-type server-rendered apps, to single-page-apps powered by APIs. A core lesson from this transition is that while standard REST and GraphQL APIs are very easy to get started with for solving client/server sync, they require significant effort and skill to scale and refine and they struggle with use-cases like multiplayer and offline support.</p>
<p>Sync engines are a robust database-grade syncing technology to ensure that data is consistent and up-to-date. It’s an ecosystem-wide refactor that many talented groups are exploring to attempt to simplify the application stack.</p>
<p>Assuming they succeed, they’ll provide a solid substrate for new types of framework that can rely on local data and rock solid sync.</p>
<h2 id="will-most-rich-client-apps-use-local-first"><a href="#will-most-rich-client-apps-use-local-first" aria-label="will most rich client apps use local first permalink" class="anchor before"></a>Will Most Rich Client Apps Use Local-First?</h2>
<p>I’ve chatted with a number of developers who — frustrated at maintaining the homegrown bespoke sync systems they wrote — are replacing it with new local-first tooling. The tooling feels a lot better and having standard primitives make it easier to build great experiences.</p>
<p>Local-first is developing into an ecosystem similar to authentication services but for handling data and features that need to be real-time, collaborative, or offline.</p>
<p>The key question for us technologists is whether local-first will remain a niche technology or if it’ll gradually replace the current API-based approach.</p>
<p>It’s still early but I’m confident that at a minimum, we’ll see multiple breakout startups along with a few healthy open-source ecosystems around the different approaches. And if local-first becomes the new default paradigm for handling data,, it will be much larger and reshape many parts of the cloud ecosystem.</p>
<p>But: there are many issues to solve first! Let’s look in detail at one of the first issues that people encounter: handling CRUD operations.</p>
<h3 id="crud-with-crdt-based-sync-engines"><a href="#crud-with-crdt-based-sync-engines" aria-label="crud with crdt based sync engines permalink" class="anchor before"></a>CRUD with CRDT-based Sync Engines</h3>
<p>To fully replace client-server APIs, sync engines need robust support for fine-grained access control and complex write validation.</p>
<p>The most basic use case for an API is <em>state transfer</em> from the server to the client. The client wants to show information about an object so reads the necessary data through the API.</p>
<p>Local-first tools all handle this perfectly. They ensure the latest object data is synced correctly to the client db for querying from the UI.</p>
<p>But while reads are generally easy, support for complex writes is still immature in local-first tooling. Clients tend to have unrestricted write access and updates are immediately synced to other clients. While this is generally fine for text collaboration or multiplayer drawing, this wouldn’t work for a typical ecommerce or SaaS application.</p>
<p>Local-first tools need somewhere to put arbitrary business logic written in code, because real-world systems start out elegant and simple and, over time, accumulate lots of messy, important chunks of logic.</p>
<p>A data property might normally be limited to 20 but Acme Corp paid $50k to get a limit of 100. Or write access needs to be restricted for sensitive information like account balances. Or writes need validation by third party APIs, like a calendar booking system that needs to ask an external calendar API if a time slot is open.</p>
<p>Normally, these inevitable chunks of code and logic live on the server behind an API. And we still need somewhere to put them in a local-first world.</p>
<p>Put differently: local-first CRDT-based sync engines can drive consistency within a system but real-world systems also need an authoritative server which can enforce consistency within external constraints and systems.</p>
<p>Or as Aaron Boodman, a <a href="https://replicache.dev/">Replicache</a> co-founder, put it: “CRDTs converge, but to where?”</p>
<h3 id="using-a-distributed-state-machine-to-handle-complex-writes"><a href="#using-a-distributed-state-machine-to-handle-complex-writes" aria-label="using a distributed state machine to handle complex writes permalink" class="anchor before"></a>Using a Distributed State Machine to Handle Complex Writes</h3>
<p>I asked Anselm Eickhoff and James Arthur (founders of <a href="https://jazz.tools/">Jazz</a> and <a href="https://electric-sql.com/">ElectricSQL</a> respectively, both CRDT-based tools) about how they suggest handling writes that need an authoritative server.</p>
<p>They both suggested emulating API request/response patterns through a distributed state machine running on a replicated object.</p>
<p>So let’s say a client wants to update the user name. It creates the following object:</p>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"machine"</span><span class="token operator">:</span> <span class="token string">"updateUserName"</span><span class="token punctuation">,</span>
<span class="token property">"state"</span><span class="token operator">:</span> <span class="token string">"requestedUpdate"</span><span class="token punctuation">,</span>
<span class="token property">"request"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
<span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span><span class="token punctuation">,</span>
<span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122496</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token property">"response"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
<span class="token punctuation">}</span>


<span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Bob"</span><span class="token punctuation">,</span>
<span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span>
<span class="token punctuation">}</span></code></pre></div>
<p>The server listens for writes and upon receiving this one, validates the request and updates the state machine object along with the name on the user object (which again are synced back to clients as the “server” is just another client):</p>
<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"machine"</span><span class="token operator">:</span> <span class="token string">"updateUserName"</span><span class="token punctuation">,</span>
<span class="token property">"state"</span><span class="token operator">:</span> <span class="token string">"finished"</span><span class="token punctuation">,</span>
<span class="token property">"request"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
<span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span><span class="token punctuation">,</span>
<span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122496</span>
<span class="token punctuation">}</span>
<span class="token property">"response"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"error"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122996</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>


<span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
<span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span>
<span class="token punctuation">}</span></code></pre></div>
<p>This has the interesting and useful implication that in-flight requests are synced to other clients and it’s trivial for the server to emit progress updates. E.g. an app that supports a team of users uploading images for some nifty AI enhancements. The client can emit progress updates on the upload and the server as it works through enhancements.</p>
<p>In other words, requests/responses have the same multiplayer, offline, real-time sync properties as the rest of the app.</p>
<p>This pattern can be wrapped up in a standard mutation function e.g. <code class="language-text">const {res, error} = await updateName({ name: "fred" })</code></p>
<p>Jazz also lets you restrict writes to certain object fields to a specific role. Sensitive information would be marked read-only for clients who would need to request the server to update them.</p>
<h3 id="local-first-dx-is-still-being-explored"><a href="#local-first-dx-is-still-being-explored" aria-label="local first dx is still being explored permalink" class="anchor before"></a>Local-First DX is Still Being Explored</h3>
<p>Beyond the question of <em>can</em> you build any application with local-first tools, there’s still the question of whether devs will <em>want</em> to.</p>
<ul>
<li>Do advantages make it worth learning a new stack and migrating applications?</li>
<li>There’s a lot of missing components still — what will a full production DX for a local-first toolchain look like?</li>
<li>How complex will it feel to build a simple app?</li>
<li>Is there enough demand to fund all the new libraries and products that’ll need to be built?</li>
</ul>
<p>The success of Figma, Superhuman, and Linear suggest these issues will get solved in time.</p>
<h2 id="what-approaches-are-people-exploring-now"><a href="#what-approaches-are-people-exploring-now" aria-label="what approaches are people exploring now permalink" class="anchor before"></a>What Approaches are People Exploring Now?</h2>
<p>I’m grouping approaches I see into three broad categories.</p>
<h3 id="1-replicated-data-structures"><a href="#1-replicated-data-structures" aria-label="1 replicated data structures permalink" class="anchor before"></a>1. Replicated Data Structures</h3>
<p>These projects provide support for replicated data structures. They are convenient building blocks for any sort of real-time or multiplayer project. They typically give you APIs similar to native Javascript maps and arrays but which guarantee state updates are replicated to other clients and to the server.</p>
<p>It feels like magic when you can build a simple application and and see changes instantly replicate between devices with no additional work.</p>
<p>Most replicated data structures rely on CRDT algorithms to merge concurrent and offline edits from multiple clients.</p>
<p>There’s a number of open source and hosted projects offering replicated data structures, including the granddaddy in this space, Firebase, plus many newer ones.</p>
<p>These services are great for making parts of an app real-time / multiplayer. E.g. a drawing surface, a chat room, a notification system, presence, etc. They’re very simple to get started with and the shared data structures approach offers a much better DX than manually passing events through with websockets or push messaging services.</p>
<p>Open source projects:</p>

<p>Hosted services:</p>

<h3 id="2-replicated-database-tables"><a href="#2-replicated-database-tables" aria-label="2 replicated database tables permalink" class="anchor before"></a>2. Replicated Database Tables</h3>
<p>An approach several projects are taking is to sync from Postgres to a client db (generally SQLite). You pick tables (or materialized views) to sync to the client and then they get loaded along with real-time updates as writes land in Postgres.</p>
<p>SQLite in the browser is one big advantage of this approach as it gives you the rich, familiar querying power of SQL in the client.</p>
<p>Given Postgres’ widespread usage and central position in most application architectures, this is a great way to start with local-first. Instead of syncing data in and out of replicated data structures, you can read and write directly to Postgres as normal, confident that clients will be in sync.</p>
<p>I’ve built a number of job queues and notification systems over the years and they’ve all struggled with their version of the Byzantine Generals problem. Clients would miss an update (usually due to being offline), and then users would complain about zombie jobs that never finished. In contrast, replicated database tables mean the background process can simply write updates to Postgres, confident all connected clients will get the update.</p>
<h4 id="postgres-to-sqlite"><a href="#postgres-to-sqlite" aria-label="postgres to sqlite permalink" class="anchor before"></a>Postgres to SQLite:</h4>

<p>ElectricSQL and Powersync support syncing client writes back to Postgres and other clients.</p>

<ul>
<li>
<p>SQLite to SQLite</p>

</li>
<li>
<p>MongoDB to Client DB</p>

</li>
</ul>
<h3 id="3-replication-as-a-protocol"><a href="#3-replication-as-a-protocol" aria-label="3 replication as a protocol permalink" class="anchor before"></a>3. Replication as a Protocol</h3>
<p>The startup <a href="https://replicache.dev/">Replicache</a> has a unique “replication as a protocol” approach. Replicache is a client JS library along with a replication protocol — which lets you integrate arbitrary backends, provided you follow the spec. It’s more upfront work, as the sync engine is “some assembly required”, but as Replicache is mostly your own code, it gives the most flexibility and power of any local-first tool I’ve seen to date. The startup behind Replicache is also building Reflect, a hosted backend for Replicache.</p>
<h2 id="soshould-you-go-local-first"><a href="#soshould-you-go-local-first" aria-label="soshould you go local first permalink" class="anchor before"></a>So…Should You Go Local-First?</h2>
<p>I think it depends on your use case and risk tolerance.</p>
<p>For the right people and teams, it’s an exciting time to jump in. Realtime, multiplayer, and offline features will significantly improve much of our day-to-day software.</p>
<p>For almost any <strong>real-time use case</strong>, I’d choose <em>replicated data structures</em> over raw web sockets as they give you a much simpler DX and robust guarantees that clients will get updates.</p>
<p>For <strong>multiplayer and offline</strong>, again you’ll almost certainly want to pick an open source <em>replicated data structure</em> or hosted service. There’s a lot of difficult problems that they help solve.</p>
<p>The <em>Replicated Database</em> approach also works for the real-time, multiplayer, offline use cases. It should be especially useful for data-heavy applications and ones with many background processing writing into Postgres.</p>
<p>But in general, I’d still be wary of using local-first outside real-time / multiplayer / offline use cases. Local-first is definitely still bleeding-edge. You will hit unexpected problems. A good community has rapidly developed, but there’ll still be some stretches on the road where you’ll have to solve novel problems.</p>
<p>So: if you need local-first, see if it makes sense to isolate the local-first parts and architect the rest of the app (for now) in a more conventional fashion.</p>
<p>I’ve found that most of the major tools have plenty of examples and demos to play with and active Discord channels. There’s also a general local-first community over at <a href="https://localfirstweb.dev/">https://localfirstweb.dev/</a> and they hold regular meetups.</p>
<p>Following along, I’m getting a lot of the same vibes that I got in the then-nascent React community circa 2014 or 2015.</p>
<p>There are also a lot of interesting implications of local-first development in the realm of privacy, decentralization, data control and so on, but I’ll leave it to others more well-versed in these topics to flesh them out.</p>
<p>And some more links below. Happy building!</p>

<p>Discuss this post on <a href="">X.com née Twitter</a>, <a href="https://warpcast.com/kam/0x0c1632">Farcaster</a>, or <a href="https://bsky.app/profile/kam.bsky.social/post/3k77qihvoip2h">Bluesky</a></p>
<p><em>Thanks to Sam Bhagwat, Shannon Soper, Johannes Schickling, Andreas Klinger, Pekka Enberg, Anselm Eickhoff, and James Arthur for reading drafts of this post</em></p>

+ 228
- 0
cache/2023/5f93f91a46391e0e120dac49298857d1/index.html 查看文件

@@ -0,0 +1,228 @@
<!doctype html><!-- This is a valid HTML5 document. -->
<!-- Screen readers, SEO, extensions and so on. -->
<html lang="fr">
<!-- Has to be within the first 1024 bytes, hence before the `title` element
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
<meta charset="utf-8">
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
<!-- The viewport meta is quite crowded and we are responsible for that.
See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- Required to make a valid HTML5 document. -->
<title>banlieue ou suburb (archive) — David Larlet</title>
<meta name="description" content="Publication mise en cache pour en conserver une trace.">
<!-- That good ol' feed, subscribe :). -->
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
<!-- Generated from https://realfavicongenerator.net/ such a mess. -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
<link rel="manifest" href="/static/david/icons2/site.webmanifest">
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
<meta name="msapplication-TileColor" content="#f7f7f7">
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
<!-- Documented, feel free to shoot an email. -->
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
<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>
<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>
<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>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<script>
function toggleTheme(themeName) {
document.documentElement.classList.toggle(
'forced-dark',
themeName === 'dark'
)
document.documentElement.classList.toggle(
'forced-light',
themeName === 'light'
)
}
const selectedTheme = localStorage.getItem('theme')
if (selectedTheme !== 'undefined') {
toggleTheme(selectedTheme)
}
</script>

<meta name="robots" content="noindex, nofollow">
<meta content="origin-when-cross-origin" name="referrer">
<!-- Canonical URL for SEO purposes -->
<link rel="canonical" href="https://www.la-grange.net/2023/03/03/suburb">

<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">


<article>
<header>
<h1>banlieue ou suburb</h1>
</header>
<nav>
<p class="center">
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="https://www.la-grange.net/2023/03/03/suburb" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<figure>
<img src="https://www.la-grange.net/2023/03/03/8983-parking.jpg" alt="Parking the centre commercial.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>Banlieue ou Suburb. La banlieue est associée au mot français avec tout son bagage social, économique et urbain. La « suburb » américaine de la Californie déploie une grammaire totalement différente.</p>
<p>Hier, je mentionnais le temps pour aller en transport en commun ou en voiture. J'aurais probablement dû mentionner la distance à pied… 11h11 pour 51km.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/route-san-diego.jpg" alt="itinéraire sur une carte électronique avec plusieurs choix possibles entre San Diego et Rancho Bernado.">
<figcaption>Itinéraire</figcaption>
</figure>
<p>Je n'ai pas le temps cette fois-ci et la fatigue des deux voyages accumulés mais je me dis qu'il me faudrait parcourir cette distance infernale à pied pour mieux comprendre l'idiotie de notre monde, pour mieux saisir les dégradés des tensions urbaines.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/route-bureau.jpg" alt="itinéraire sur une carte électronique avec plusieurs choix possibles entre l'hôtel et le bureau.">
<figcaption>Itinéraire jusqu'au bureau</figcaption>
</figure>
<p>Quand j'ai mentionné que j'allais marcher de l'hôtel jusqu'au bureau, j'ai senti de l'incompréhension. Et pourtant il ne s'agit là que de 40 minutes… pour quelque chose qui est probablement à 15 minutes en ligne droite.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/8991-autoroute.jpg" alt="Passage sous la structure en béton de l'autoroute.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>J'ai tout de même décider de couper à travers les collines et les propriétés industrielles. J'ai gardé l'œil ouvert pour les serpents et les flics. Je me suis gavé du parfum des pins et des eucalyptus. J'ai observé les cactus dans la rocaille. En fait ma plus grande inquiétude dans ses parcours hors-circuits sont les autres humains, ceux qui ne comprendraient pas ma démarche.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/8994-collines.jpg" alt="végétation et longue clotûre en métal avec barbelés dans les collines.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>Autoroute et propriétés abandonnées de sites industriels rendent le parcours une longue absurdité. Et bien sûr pas de transports en commun.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/8998-bureau.jpg" alt="Vue sur des parkings vides.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>Finalement en suivant la clôture, j'ai trouvé un passage sur un parking abandonné que j'ai pu photographier depuis le bureau, une fois arrivé. Il a fallu avant cela traverser encore une grande route sans humanité.</p>
<p>Tout cela pose tellement de questions, incite à une mise en perspective sur notre rôle dans le monde.</p>
<p>Les écrivains-marcheurs-photographes devraient faire des résidences d'artiste sur les sites industriels de la Californie.</p>
<p>Je suis sûr qu'il y a plus que juste la surface de ces désolations culturelles. Et que l'on ne se méprenne, la marche dans cette trajectoire existensialiste est ce qui me donne l'énergie, le plaisir.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/9002-ombre.jpg" alt="Ombre de mon corps sur un parking vide, les montagnes sont au loin, quelques arbres éparpillés.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>Le soir, je suis rentré de nouveau à pied.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/9006-north.jpg" alt="Panneau de direction nord accroché à une rampe en béton pour l'autoroute 15.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<hr>
<blockquote>
<p><a href="https://mastodon.cloud/@karlcow/109957206842654554">Suburban America</a>. How depressing can you be?</p>
</blockquote>
<hr>
<p>Je ne suis pas sûr de savoir les choses que je veux d'un emploi. Peut-être, suis-je inquiet de restreindre trop la possibilité d'explorer de nouvelles trajectoires ?</p>
<p><a href="https://lynnandtonic.com/thoughts/entries/unordered-incomplete-list-of-things-i-want-from-a-job/">Unordered, incomplete list of things I want from a job</a></p>
</article>


<hr>

<footer>
<p>
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
</svg> Suivre</a> •
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
</svg> Pro</a> •
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
</svg> Email</a> •
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
</svg> Légal</abbr>
</p>
<template id="theme-selector">
<form>
<fieldset>
<legend><svg class="icon icon-brightness-contrast">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
</svg> Thème</legend>
<label>
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
</label>
<label>
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé
</label>
<label>
<input type="radio" value="light" name="chosen-color-scheme"> Clair
</label>
</fieldset>
</form>
</template>
</footer>
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
<script>
function loadThemeForm(templateName) {
const themeSelectorTemplate = document.querySelector(templateName)
const form = themeSelectorTemplate.content.firstElementChild
themeSelectorTemplate.replaceWith(form)

form.addEventListener('change', (e) => {
const chosenColorScheme = e.target.value
localStorage.setItem('theme', chosenColorScheme)
toggleTheme(chosenColorScheme)
})

const selectedTheme = localStorage.getItem('theme')
if (selectedTheme && selectedTheme !== 'undefined') {
form.querySelector(`[value="${selectedTheme}"]`).checked = true
}
}

const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
window.addEventListener('load', () => {
let hasDarkRules = false
for (const styleSheet of Array.from(document.styleSheets)) {
let mediaRules = []
for (const cssRule of styleSheet.cssRules) {
if (cssRule.type !== CSSRule.MEDIA_RULE) {
continue
}
// WARNING: Safari does not have/supports `conditionText`.
if (cssRule.conditionText) {
if (cssRule.conditionText !== prefersColorSchemeDark) {
continue
}
} else {
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
continue
}
}
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
}

// WARNING: do not try to insert a Rule to a styleSheet you are
// currently iterating on, otherwise the browser will be stuck
// in a infinite loop…
for (const mediaRule of mediaRules) {
styleSheet.insertRule(mediaRule.cssText)
hasDarkRules = true
}
}
if (hasDarkRules) {
loadThemeForm('#theme-selector')
}
})
</script>
</body>
</html>

+ 61
- 0
cache/2023/5f93f91a46391e0e120dac49298857d1/index.md 查看文件

@@ -0,0 +1,61 @@
title: banlieue ou suburb
url: https://www.la-grange.net/2023/03/03/suburb
hash_url: 5f93f91a46391e0e120dac49298857d1

<figure>
<img src="https://www.la-grange.net/2023/03/03/8983-parking.jpg" alt="Parking the centre commercial.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>Banlieue ou Suburb. La banlieue est associée au mot français avec tout son bagage social, économique et urbain. La « suburb » américaine de la Californie déploie une grammaire totalement différente.</p>
<p>Hier, je mentionnais le temps pour aller en transport en commun ou en voiture. J'aurais probablement dû mentionner la distance à pied… 11h11 pour 51km.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/route-san-diego.jpg" alt="itinéraire sur une carte électronique avec plusieurs choix possibles entre San Diego et Rancho Bernado.">
<figcaption>Itinéraire</figcaption>
</figure>
<p>Je n'ai pas le temps cette fois-ci et la fatigue des deux voyages accumulés mais je me dis qu'il me faudrait parcourir cette distance infernale à pied pour mieux comprendre l'idiotie de notre monde, pour mieux saisir les dégradés des tensions urbaines.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/route-bureau.jpg" alt="itinéraire sur une carte électronique avec plusieurs choix possibles entre l'hôtel et le bureau.">
<figcaption>Itinéraire jusqu'au bureau</figcaption>
</figure>
<p>Quand j'ai mentionné que j'allais marcher de l'hôtel jusqu'au bureau, j'ai senti de l'incompréhension. Et pourtant il ne s'agit là que de 40 minutes… pour quelque chose qui est probablement à 15 minutes en ligne droite.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/8991-autoroute.jpg" alt="Passage sous la structure en béton de l'autoroute.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>J'ai tout de même décider de couper à travers les collines et les propriétés industrielles. J'ai gardé l'œil ouvert pour les serpents et les flics. Je me suis gavé du parfum des pins et des eucalyptus. J'ai observé les cactus dans la rocaille. En fait ma plus grande inquiétude dans ses parcours hors-circuits sont les autres humains, ceux qui ne comprendraient pas ma démarche.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/8994-collines.jpg" alt="végétation et longue clotûre en métal avec barbelés dans les collines.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>Autoroute et propriétés abandonnées de sites industriels rendent le parcours une longue absurdité. Et bien sûr pas de transports en commun.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/8998-bureau.jpg" alt="Vue sur des parkings vides.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>Finalement en suivant la clôture, j'ai trouvé un passage sur un parking abandonné que j'ai pu photographier depuis le bureau, une fois arrivé. Il a fallu avant cela traverser encore une grande route sans humanité.</p>
<p>Tout cela pose tellement de questions, incite à une mise en perspective sur notre rôle dans le monde.</p>
<p>Les écrivains-marcheurs-photographes devraient faire des résidences d'artiste sur les sites industriels de la Californie.</p>
<p>Je suis sûr qu'il y a plus que juste la surface de ces désolations culturelles. Et que l'on ne se méprenne, la marche dans cette trajectoire existensialiste est ce qui me donne l'énergie, le plaisir.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/9002-ombre.jpg" alt="Ombre de mon corps sur un parking vide, les montagnes sont au loin, quelques arbres éparpillés.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<p>Le soir, je suis rentré de nouveau à pied.</p>

<figure>
<img src="https://www.la-grange.net/2023/03/03/9006-north.jpg" alt="Panneau de direction nord accroché à une rampe en béton pour l'autoroute 15.">
<figcaption>Rancho Bernardo, Californie, 3 mars 2023</figcaption>
</figure>
<hr>
<blockquote>
<p><a href="https://mastodon.cloud/@karlcow/109957206842654554">Suburban America</a>. How depressing can you be?</p>
</blockquote>
<hr>
<p>Je ne suis pas sûr de savoir les choses que je veux d'un emploi. Peut-être, suis-je inquiet de restreindre trop la possibilité d'explorer de nouvelles trajectoires ?</p>
<p><a href="https://lynnandtonic.com/thoughts/entries/unordered-incomplete-list-of-things-i-want-from-a-job/">Unordered, incomplete list of things I want from a job</a></p>

+ 222
- 0
cache/2023/b2292d98e9d54537c13b8c1e2cae5583/index.html 查看文件

@@ -0,0 +1,222 @@
<!doctype html><!-- This is a valid HTML5 document. -->
<!-- Screen readers, SEO, extensions and so on. -->
<html lang="fr">
<!-- Has to be within the first 1024 bytes, hence before the `title` element
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
<meta charset="utf-8">
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
<!-- The viewport meta is quite crowded and we are responsible for that.
See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- Required to make a valid HTML5 document. -->
<title>Writers and talkers and leaders, oh my! (archive) — David Larlet</title>
<meta name="description" content="Publication mise en cache pour en conserver une trace.">
<!-- That good ol' feed, subscribe :). -->
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
<!-- Generated from https://realfavicongenerator.net/ such a mess. -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
<link rel="manifest" href="/static/david/icons2/site.webmanifest">
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
<meta name="msapplication-TileColor" content="#f7f7f7">
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
<!-- Documented, feel free to shoot an email. -->
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
<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>
<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>
<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>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<script>
function toggleTheme(themeName) {
document.documentElement.classList.toggle(
'forced-dark',
themeName === 'dark'
)
document.documentElement.classList.toggle(
'forced-light',
themeName === 'light'
)
}
const selectedTheme = localStorage.getItem('theme')
if (selectedTheme !== 'undefined') {
toggleTheme(selectedTheme)
}
</script>

<meta name="robots" content="noindex, nofollow">
<meta content="origin-when-cross-origin" name="referrer">
<!-- Canonical URL for SEO purposes -->
<link rel="canonical" href="https://everythingchanges.us/blog/writers-and-talkers-and-leaders/">

<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">


<article>
<header>
<h1>Writers and talkers and leaders, oh my!</h1>
</header>
<nav>
<p class="center">
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="https://everythingchanges.us/blog/writers-and-talkers-and-leaders/" title="Lien vers le contenu original">Source originale</a>
</p>
</nav>
<hr>
<p>I ONCE OBSERVED a small group of leaders gather a bunch of their team together to hash out some big changes to their organization’s mission. They rented space away from the office, flew the right people in, cleared everyone’s calendars. They booked a big table at a very good restaurant. And then they proceeded to talk. They, and the other folks they invited in, talked for the better part of two days. There were breakout groups and there were plenty of breaks, mind you. Coffee and snacks and bottled water and some very good meals were on hand. But the activity everyone had gathered to participate in was talking.</p>

<p>These leaders were <em>talkers</em>. At the end of the second day of this, they were amped up and excited about the plans that had been hashed out and the new vision they’d aligned on and the potential for really amazing change that was afoot. There might have been high fives as they grabbed their coats and headed home. There were definitely some excited DMs and several <em>that was so great!</em> messages sent from the trains and planes that carried them off.</p>

<p>What they didn’t realize was that among the talkers who had been gathered were several <em>writers.</em> The writers were on the whole befuddled and exhausted; they weren’t sure what had been decided on, and when they tried to reflect on all that talking, it was a blur. They could feel the energy of the room was such that something exciting had happened but they didn’t quite know what to think of it. They were uncertain if they had made themselves clear; they were uncertain of what they had wanted to make clear. They wondered if they were missing something, but they couldn’t articulate what it was. They too sent thanks and thumbs up emojis, but they went home with a vague sense of dread.</p>

<h1 id="modes">Modes</h1>
<p>Are you a writer or a talker?</p>

<p>That is, when you need to think about something, do you generally reach for something to write with, or look for someone to talk to?</p>

<p>I first encountered this concept in Peter Drucker’s good and very short <em><a href="https://aworkinglibrary.com/reading/managing-oneself">Managing Oneself</a></em>. He refers to <em>readers</em> and <em>listeners</em>—that is, to preferred modes of learning. I’m turning that around to look at modes of <em>thinking</em>, because in my experience, it’s those different modes of thinking that give rise to some of the most unattended to but tricky workplace conflicts. But both perspectives are useful. Readers learn by reading and think by writing; listeners learn by listening and think by talking.</p>

<p>Most people default to one or another behavior but rarely use them exclusively. Writers will often benefit from talking things out when they get stuck; and talkers will find that occasionally writing something down helps solidify their thoughts. Both strategies can be learned. Whether you’re a writer or a talker isn’t about your inability to do one or the other so much as it is a preferred or optimized mode.</p>

<p>This is, incidentally, a much more valuable way of understanding different working styles than the old maker vs manager canard. Both talkers and writers make things (including <a href="https://aworkinglibrary.com/writing/making-decisions">decisions</a>), but they means by which they make things—and the needs they have in relation to their colleagues—are not the same.</p>

<h1 id="power-talks">Power talks</h1>
<p>In most orgs, talkers are overrepresented among the leadership. This is not because talking offers any advantages over writing in terms of thinking power. Rather, it’s that most of our models for leadership—meetings, town halls, presentations, interviews—privilege talkers. Writers who move up in organizations that make strong use of these models have to either become adept at working outside their comfort zones or else influence the organization to become more writer-friendly. Since the latter requires interrupting the existing structures of power, it’s by definition more challenging to pull off, and less likely to be the route that any eager writer will take.</p>

<p>The result is that a great many orgs have talkers at the top and writers down below, but because power obscures difference, the talkers are very rarely aware of this setup.</p>

<p>What the leaders I observed did was optimize for their own mode of thinking. This isn’t a bad strategy per se, and it’s something that most leaders need to do at least some of the time in order to be effective. But in the course of that optimization, they effectively disenfranchised most of the writers among them. They left a lot of good brain power and potential alignment on the floor, and they didn’t even realize it was there as they stepped over it on the way out the door.</p>

<h1 id="bothand">Both/and</h1>
<p>I’m a writer more than I’m a talker, but I’m not here to argue for one mode over the other. I’m quite certain good thinking happens in both modes. What I am here to do is point out that whichever mode you gravitate to, you work with someone else who leans the other way. And if you do not acknowledge that and work through it, you are wasting a lot of time.</p>

<p>If you, a talker, have a close peer who is a writer, but every time you need their input you put time on their calendar to talk it out, you are not getting their best thinking. Your peer is likely to become convinced that you are confusing and challenging to work with.</p>

<p>If you, a writer, report to a talker, but you insist on communicating primarily through documents, you are—and I say this with affection, because lo, I have been there—fucked. Your manager will never fully grok what it is you’re trying to do, and you will never understand your manager.</p>

<p>If a talker responds to lack of alignment by scheduling time for discussion, but doesn’t also provide a space for the writers to work out their thoughts in writing, whatever alignment seems to have emerged in those meetings will have a half life of hours.</p>

<p>If, on the other hand, a writer tries to build alignment by bombing a bunch of talkers with a well-outlined set of docs, I wish them well but also encourage them to get ready to run. Because as soon as the talkers open their mouths, the message from those docs is going to diverge, and if the writer doesn’t follow them into those discussions, they will be left in the dust.</p>

<h1 id="inquiry-over-process">Inquiry over process</h1>
<p>Okay, so once you know about these different modes, what do you do about it?</p>

<p>Talkers need to recognize that not everyone loves to think out loud, and that giving space for writing is part of what it means to make use of the best brains around you. Writers need to remember that writing isn’t some perfected ideal of thinking and that making space for the messy, chaotic, and improvisational work of talking things out is often exactly what a team needs to create change. Whichever mode you prefer, it’s not feasible to abstain from the other; doing good, collaborative work requires that you practice both modes.</p>

<p>Here is where I could propose some tactics and techniques that work to close the gaps. And, sure, I can do that: you can foreshadow a scheduled discussion by dropping some questions or ideas that you want to talk out into a doc, and ask everyone to write up a few notes ahead of time. Or instead of drafting a whole-assed copyedited and formatted document to share, first write up an outline or a few short notes about the thing you intend to draft, and then open that up for input—both in the doc itself and in some short synchronous conversations.</p>

<p>But more than any specific process I want to posit that the real trick to getting the best out of all the brains around you is to <em>ask what people need</em> and then draw from whichever tactic will meet that need in that moment. Get in the habit of asking questions like: has everyone had a chance to think this out? Do you need more space to talk or write or something else? Can you share what you understand has been decided here today, and why? And then respond without judgement to whatever emerges.</p>

<p>The goal here isn’t some idealized set of processes that perfectly enfranchises everyone on the team every time. The goal is a collective sensitivity and maturity to the different modes and to the circumstances of the topic that lets people safely inquire into what they and their teammates need, and then to productively negotiate the best way to attend to those needs.</p>

<p>You may be thinking that this is going to take a lot more time but I’m here to tell you it will not. If at the moment you are haphazardly and unknowingly optimizing for only one mode of thinking, you are also unknowingly optimizing for confusion and misunderstanding and second-guessing. Because the efficiency of communication isn’t solely a measure of the time it takes to move information from one head to another; it’s also the time <em>and energy</em> required to build and sustain collective understanding.</p>

<p>And here’s the best part: if you and your team strengthen some muscles for asking after what people need when it comes to thinking through a plan or decision or whathaveyou, you will have simultaneously developed a new set of superpowers for building clarity and navigating change and working through conflict. And it’s so easy to start: just say to the next person you interact with, “Hey, when I need to think about something, I like to [talk/write] about it. How about you?” Then talk out whatever they tell you.</p>

<p>Just kidding, go put that information in a doc where it belongs.</p>
</article>


<hr>

<footer>
<p>
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
</svg> Suivre</a> •
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
</svg> Pro</a> •
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
</svg> Email</a> •
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
</svg> Légal</abbr>
</p>
<template id="theme-selector">
<form>
<fieldset>
<legend><svg class="icon icon-brightness-contrast">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
</svg> Thème</legend>
<label>
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
</label>
<label>
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé
</label>
<label>
<input type="radio" value="light" name="chosen-color-scheme"> Clair
</label>
</fieldset>
</form>
</template>
</footer>
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
<script>
function loadThemeForm(templateName) {
const themeSelectorTemplate = document.querySelector(templateName)
const form = themeSelectorTemplate.content.firstElementChild
themeSelectorTemplate.replaceWith(form)

form.addEventListener('change', (e) => {
const chosenColorScheme = e.target.value
localStorage.setItem('theme', chosenColorScheme)
toggleTheme(chosenColorScheme)
})

const selectedTheme = localStorage.getItem('theme')
if (selectedTheme && selectedTheme !== 'undefined') {
form.querySelector(`[value="${selectedTheme}"]`).checked = true
}
}

const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
window.addEventListener('load', () => {
let hasDarkRules = false
for (const styleSheet of Array.from(document.styleSheets)) {
let mediaRules = []
for (const cssRule of styleSheet.cssRules) {
if (cssRule.type !== CSSRule.MEDIA_RULE) {
continue
}
// WARNING: Safari does not have/supports `conditionText`.
if (cssRule.conditionText) {
if (cssRule.conditionText !== prefersColorSchemeDark) {
continue
}
} else {
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
continue
}
}
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
}

// WARNING: do not try to insert a Rule to a styleSheet you are
// currently iterating on, otherwise the browser will be stuck
// in a infinite loop…
for (const mediaRule of mediaRules) {
styleSheet.insertRule(mediaRule.cssText)
hasDarkRules = true
}
}
if (hasDarkRules) {
loadThemeForm('#theme-selector')
}
})
</script>
</body>
</html>

+ 55
- 0
cache/2023/b2292d98e9d54537c13b8c1e2cae5583/index.md 查看文件

@@ -0,0 +1,55 @@
title: Writers and talkers and leaders, oh my!
url: https://everythingchanges.us/blog/writers-and-talkers-and-leaders/
hash_url: b2292d98e9d54537c13b8c1e2cae5583

<p>I ONCE OBSERVED a small group of leaders gather a bunch of their team together to hash out some big changes to their organization’s mission. They rented space away from the office, flew the right people in, cleared everyone’s calendars. They booked a big table at a very good restaurant. And then they proceeded to talk. They, and the other folks they invited in, talked for the better part of two days. There were breakout groups and there were plenty of breaks, mind you. Coffee and snacks and bottled water and some very good meals were on hand. But the activity everyone had gathered to participate in was talking.</p>

<p>These leaders were <em>talkers</em>. At the end of the second day of this, they were amped up and excited about the plans that had been hashed out and the new vision they’d aligned on and the potential for really amazing change that was afoot. There might have been high fives as they grabbed their coats and headed home. There were definitely some excited DMs and several <em>that was so great!</em> messages sent from the trains and planes that carried them off.</p>

<p>What they didn’t realize was that among the talkers who had been gathered were several <em>writers.</em> The writers were on the whole befuddled and exhausted; they weren’t sure what had been decided on, and when they tried to reflect on all that talking, it was a blur. They could feel the energy of the room was such that something exciting had happened but they didn’t quite know what to think of it. They were uncertain if they had made themselves clear; they were uncertain of what they had wanted to make clear. They wondered if they were missing something, but they couldn’t articulate what it was. They too sent thanks and thumbs up emojis, but they went home with a vague sense of dread.</p>

<h1 id="modes">Modes</h1>
<p>Are you a writer or a talker?</p>

<p>That is, when you need to think about something, do you generally reach for something to write with, or look for someone to talk to?</p>

<p>I first encountered this concept in Peter Drucker’s good and very short <em><a href="https://aworkinglibrary.com/reading/managing-oneself">Managing Oneself</a></em>. He refers to <em>readers</em> and <em>listeners</em>—that is, to preferred modes of learning. I’m turning that around to look at modes of <em>thinking</em>, because in my experience, it’s those different modes of thinking that give rise to some of the most unattended to but tricky workplace conflicts. But both perspectives are useful. Readers learn by reading and think by writing; listeners learn by listening and think by talking.</p>

<p>Most people default to one or another behavior but rarely use them exclusively. Writers will often benefit from talking things out when they get stuck; and talkers will find that occasionally writing something down helps solidify their thoughts. Both strategies can be learned. Whether you’re a writer or a talker isn’t about your inability to do one or the other so much as it is a preferred or optimized mode.</p>

<p>This is, incidentally, a much more valuable way of understanding different working styles than the old maker vs manager canard. Both talkers and writers make things (including <a href="https://aworkinglibrary.com/writing/making-decisions">decisions</a>), but they means by which they make things—and the needs they have in relation to their colleagues—are not the same.</p>

<h1 id="power-talks">Power talks</h1>
<p>In most orgs, talkers are overrepresented among the leadership. This is not because talking offers any advantages over writing in terms of thinking power. Rather, it’s that most of our models for leadership—meetings, town halls, presentations, interviews—privilege talkers. Writers who move up in organizations that make strong use of these models have to either become adept at working outside their comfort zones or else influence the organization to become more writer-friendly. Since the latter requires interrupting the existing structures of power, it’s by definition more challenging to pull off, and less likely to be the route that any eager writer will take.</p>

<p>The result is that a great many orgs have talkers at the top and writers down below, but because power obscures difference, the talkers are very rarely aware of this setup.</p>

<p>What the leaders I observed did was optimize for their own mode of thinking. This isn’t a bad strategy per se, and it’s something that most leaders need to do at least some of the time in order to be effective. But in the course of that optimization, they effectively disenfranchised most of the writers among them. They left a lot of good brain power and potential alignment on the floor, and they didn’t even realize it was there as they stepped over it on the way out the door.</p>

<h1 id="bothand">Both/and</h1>
<p>I’m a writer more than I’m a talker, but I’m not here to argue for one mode over the other. I’m quite certain good thinking happens in both modes. What I am here to do is point out that whichever mode you gravitate to, you work with someone else who leans the other way. And if you do not acknowledge that and work through it, you are wasting a lot of time.</p>

<p>If you, a talker, have a close peer who is a writer, but every time you need their input you put time on their calendar to talk it out, you are not getting their best thinking. Your peer is likely to become convinced that you are confusing and challenging to work with.</p>

<p>If you, a writer, report to a talker, but you insist on communicating primarily through documents, you are—and I say this with affection, because lo, I have been there—fucked. Your manager will never fully grok what it is you’re trying to do, and you will never understand your manager.</p>

<p>If a talker responds to lack of alignment by scheduling time for discussion, but doesn’t also provide a space for the writers to work out their thoughts in writing, whatever alignment seems to have emerged in those meetings will have a half life of hours.</p>

<p>If, on the other hand, a writer tries to build alignment by bombing a bunch of talkers with a well-outlined set of docs, I wish them well but also encourage them to get ready to run. Because as soon as the talkers open their mouths, the message from those docs is going to diverge, and if the writer doesn’t follow them into those discussions, they will be left in the dust.</p>

<h1 id="inquiry-over-process">Inquiry over process</h1>
<p>Okay, so once you know about these different modes, what do you do about it?</p>

<p>Talkers need to recognize that not everyone loves to think out loud, and that giving space for writing is part of what it means to make use of the best brains around you. Writers need to remember that writing isn’t some perfected ideal of thinking and that making space for the messy, chaotic, and improvisational work of talking things out is often exactly what a team needs to create change. Whichever mode you prefer, it’s not feasible to abstain from the other; doing good, collaborative work requires that you practice both modes.</p>

<p>Here is where I could propose some tactics and techniques that work to close the gaps. And, sure, I can do that: you can foreshadow a scheduled discussion by dropping some questions or ideas that you want to talk out into a doc, and ask everyone to write up a few notes ahead of time. Or instead of drafting a whole-assed copyedited and formatted document to share, first write up an outline or a few short notes about the thing you intend to draft, and then open that up for input—both in the doc itself and in some short synchronous conversations.</p>

<p>But more than any specific process I want to posit that the real trick to getting the best out of all the brains around you is to <em>ask what people need</em> and then draw from whichever tactic will meet that need in that moment. Get in the habit of asking questions like: has everyone had a chance to think this out? Do you need more space to talk or write or something else? Can you share what you understand has been decided here today, and why? And then respond without judgement to whatever emerges.</p>

<p>The goal here isn’t some idealized set of processes that perfectly enfranchises everyone on the team every time. The goal is a collective sensitivity and maturity to the different modes and to the circumstances of the topic that lets people safely inquire into what they and their teammates need, and then to productively negotiate the best way to attend to those needs.</p>

<p>You may be thinking that this is going to take a lot more time but I’m here to tell you it will not. If at the moment you are haphazardly and unknowingly optimizing for only one mode of thinking, you are also unknowingly optimizing for confusion and misunderstanding and second-guessing. Because the efficiency of communication isn’t solely a measure of the time it takes to move information from one head to another; it’s also the time <em>and energy</em> required to build and sustain collective understanding.</p>

<p>And here’s the best part: if you and your team strengthen some muscles for asking after what people need when it comes to thinking through a plan or decision or whathaveyou, you will have simultaneously developed a new set of superpowers for building clarity and navigating change and working through conflict. And it’s so easy to start: just say to the next person you interact with, “Hey, when I need to think about something, I like to [talk/write] about it. How about you?” Then talk out whatever they tell you.</p>

<p>Just kidding, go put that information in a doc where it belongs.</p>

+ 8
- 0
cache/2023/index.html 查看文件

@@ -71,6 +71,8 @@
<li><a href="/david/cache/2022/e44bfaaecad989f67cb2032fac000276/" title="Accès à l’article dans le cache local : The Hippocratic License">The Hippocratic License</a> (<a href="https://firstdonoharm.dev/" title="Accès à l’article original distant : The Hippocratic License">original</a>)</li>
<li><a href="/david/cache/2022/b2292d98e9d54537c13b8c1e2cae5583/" title="Accès à l’article dans le cache local : Writers and talkers and leaders, oh my!">Writers and talkers and leaders, oh my!</a> (<a href="https://everythingchanges.us/blog/writers-and-talkers-and-leaders/" title="Accès à l’article original distant : Writers and talkers and leaders, oh my!">original</a>)</li>
<li><a href="/david/cache/2022/83c60dd85e9f0f07bf41821a2694a0e5/" title="Accès à l’article dans le cache local : Shining a Light on the Digital Dark Age">Shining a Light on the Digital Dark Age</a> (<a href="https://longnow.org/ideas/shining-a-light-on-the-digital-dark-age/" title="Accès à l’article original distant : Shining a Light on the Digital Dark Age">original</a>)</li>
<li><a href="/david/cache/2022/e1a26da20c603d214d0f844d5836569e/" title="Accès à l’article dans le cache local : my mind is full of webs">my mind is full of webs</a> (<a href="https://winnielim.org/journal/my-mind-is-full-of-webs/" title="Accès à l’article original distant : my mind is full of webs">original</a>)</li>
@@ -91,6 +93,8 @@
<li><a href="/david/cache/2022/f8b7c3246cf1d4e06c735ee163be32a0/" title="Accès à l’article dans le cache local : The Content Management System of my Dreams (part 2) - The trouble with dynamic publishing">The Content Management System of my Dreams (part 2) - The trouble with dynamic publishing</a> (<a href="https://www.padawan.info/en/2023/02/the-content-management-system-of-my-dreams-part-2-the-trouble-with-dynamic-publishing.html" title="Accès à l’article original distant : The Content Management System of my Dreams (part 2) - The trouble with dynamic publishing">original</a>)</li>
<li><a href="/david/cache/2022/49f2ce04dd0beb94dc2f662163bc6339/" title="Accès à l’article dans le cache local : Some notes on Local-First Development">Some notes on Local-First Development</a> (<a href="https://bricolage.io/some-notes-on-local-first-development/" title="Accès à l’article original distant : Some notes on Local-First Development">original</a>)</li>
<li><a href="/david/cache/2022/78d79db0da7f60c48a02cfd088885085/" title="Accès à l’article dans le cache local : The (extremely) loud minority">The (extremely) loud minority</a> (<a href="https://andy-bell.co.uk/the-extremely-loud-minority/" title="Accès à l’article original distant : The (extremely) loud minority">original</a>)</li>
<li><a href="/david/cache/2022/c45d25b1d1062fcf10fbf7caaf9e21b1/" title="Accès à l’article dans le cache local : Exercices (de feuille) de styles">Exercices (de feuille) de styles</a> (<a href="https://blog.professeurjoachim.com/billet/2023-01-05-exercices-de-feuille-de-styles" title="Accès à l’article original distant : Exercices (de feuille) de styles">original</a>)</li>
@@ -213,6 +217,8 @@
<li><a href="/david/cache/2022/63654b08ad9eda03b6bea8d1f82e2843/" title="Accès à l’article dans le cache local : Yearnotes #3 • détour.studio">Yearnotes #3 • détour.studio</a> (<a href="https://détour.studio/yearnotes/3/" title="Accès à l’article original distant : Yearnotes #3 • détour.studio">original</a>)</li>
<li><a href="/david/cache/2022/5f93f91a46391e0e120dac49298857d1/" title="Accès à l’article dans le cache local : banlieue ou suburb">banlieue ou suburb</a> (<a href="https://www.la-grange.net/2023/03/03/suburb" title="Accès à l’article original distant : banlieue ou suburb">original</a>)</li>
<li><a href="/david/cache/2022/58bdc0bd6ed37d5990d24384ee40022b/" title="Accès à l’article dans le cache local : Visualized: The 4 Billion Year Path of Human Evolution">Visualized: The 4 Billion Year Path of Human Evolution</a> (<a href="https://www.visualcapitalist.com/path-of-human-evolution/" title="Accès à l’article original distant : Visualized: The 4 Billion Year Path of Human Evolution">original</a>)</li>
<li><a href="/david/cache/2022/a09b5bf450d2cf86fb9e9d6f13b070e0/" title="Accès à l’article dans le cache local : Clever Code Considered Harmful">Clever Code Considered Harmful</a> (<a href="https://www.joshwcomeau.com/career/clever-code-considered-harmful/" title="Accès à l’article original distant : Clever Code Considered Harmful">original</a>)</li>
@@ -313,6 +319,8 @@
<li><a href="/david/cache/2022/eebbf1a999fdf5c8aa80b65eccd9c48a/" title="Accès à l’article dans le cache local : Automating podcast transcripts on my Mac with OpenAI Whisper">Automating podcast transcripts on my Mac with OpenAI Whisper</a> (<a href="https://sixcolors.com/post/2023/02/automating-podcast-transcripts-on-my-mac-with-openai-whisper/" title="Accès à l’article original distant : Automating podcast transcripts on my Mac with OpenAI Whisper">original</a>)</li>
<li><a href="/david/cache/2022/2074a4d527220f5ddf2dc0b4e678c83a/" title="Accès à l’article dans le cache local : Classic rock, Mario Kart, and why we can’t agree on Tailwind">Classic rock, Mario Kart, and why we can’t agree on Tailwind</a> (<a href="https://joshcollinsworth.com/blog/tailwind-is-smart-steering" title="Accès à l’article original distant : Classic rock, Mario Kart, and why we can’t agree on Tailwind">original</a>)</li>
<li><a href="/david/cache/2022/8cb87dbe21c3f5a7a69735a70daf51c3/" title="Accès à l’article dans le cache local : Some thoughts on how to make a book, three months after I made one">Some thoughts on how to make a book, three months after I made one</a> (<a href="https://www.baldurbjarnason.com/2023/how-i-made-my-book/" title="Accès à l’article original distant : Some thoughts on how to make a book, three months after I made one">original</a>)</li>
</ul>

Loading…
取消
儲存