<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>README — frontend</title>
<link>https://readme.news/tags/frontend/</link>
<atom:link href="https://readme.news/tags/frontend/feed.xml" rel="self" type="application/rss+xml"/>
<description>README pieces tagged frontend.</description>
<language>en-us</language>
<lastBuildDate>Thu, 01 Oct 2026 13:20:31 +0000</lastBuildDate>
<item><title>The performance budget</title><link>https://readme.news/the-performance-budget/</link><guid isPermaLink="true">https://readme.news/the-performance-budget/</guid><pubDate>Mon, 27 Jul 2026 09:00:00 +0000</pubDate><description>A number, agreed in advance, that fails the build. The only mechanism that has ever kept a system fast.</description><content:encoded><![CDATA[<p>Every system starts fast and gets slower. Not because of one bad decision — because of a hundred small ones, each of which added two milliseconds, none of which anyone could reasonably object to.</p>
<p>The only mechanism I have seen reliably prevent this is a budget: a number, agreed in advance, that fails the build when exceeded.</p>
<h2 id="why-we-should-keep-it-fast-does-not-work">why "we should keep it fast" does not work<a class="anchor" href="#why-we-should-keep-it-fast-does-not-work" aria-label="link to this section">#</a></h2>
<p>Because "fast" has no threshold, so there is never a moment where a specific change is the problem.</p>
<p>Every individual addition is defensible. The library is 12 KB and saves a week. The query is 8 ms and enables a feature. The middleware is 3 ms and improves security.</p>
<p>Ten of those and your page is 400 ms slower, and no single change was wrong. There was no point at which anyone could say no, because the comparison was always "this change versus nothing" rather than "this change versus the budget."</p>
<p>A budget changes the comparison. Now the question is "what are you willing to remove to make room for this," which is a real conversation.</p>
<h2 id="setting-the-numbers">setting the numbers<a class="anchor" href="#setting-the-numbers" aria-label="link to this section">#</a></h2>
<p>Derive them from user-facing outcomes, not from what you currently have.</p>
<p><strong>For a web frontend:</strong></p>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">metric</th><th style="text-align:left">budget</th></tr></thead><tbody><tr><td style="text-align:left">JavaScript, compressed</td><td style="text-align:left">170 KB</td></tr><tr><td style="text-align:left">CSS, compressed</td><td style="text-align:left">60 KB</td></tr><tr><td style="text-align:left">Largest Contentful Paint (p75, mobile)</td><td style="text-align:left">2.5 s</td></tr><tr><td style="text-align:left">Interaction to Next Paint (p75)</td><td style="text-align:left">200 ms</td></tr><tr><td style="text-align:left">Total requests, initial load</td><td style="text-align:left">40</td></tr></tbody></table></div>
<p><strong>For an API:</strong></p>
<div class="table-wrap"><table><thead><tr><th style="text-align:left">metric</th><th style="text-align:left">budget</th></tr></thead><tbody><tr><td style="text-align:left">p50 latency</td><td style="text-align:left">50 ms</td></tr><tr><td style="text-align:left">p99 latency</td><td style="text-align:left">500 ms</td></tr><tr><td style="text-align:left">database queries per request</td><td style="text-align:left">10</td></tr><tr><td style="text-align:left">memory per instance</td><td style="text-align:left">512 MB</td></tr></tbody></table></div>
<p><strong>Per-layer budgets are the underrated part.</strong> A single end-to-end number tells you something regressed. Per-layer budgets tell you <em>where</em>.</p>
<div class="code"><pre><code>total request budget: 200 ms
  auth:        10 ms
  validation:   5 ms
  database:    80 ms
  business:    40 ms
  serialize:   15 ms
  overhead:    50 ms</code></pre></div>
<p>When the database layer goes to 120 ms, that specific budget fails, and the person who changed the query knows immediately rather than in a month when someone investigates a general slowdown.</p>
<p>This is borrowed directly from game development, where per-subsystem frame budgets have been standard practice for decades.</p>
<h2 id="enforcement">enforcement<a class="anchor" href="#enforcement" aria-label="link to this section">#</a></h2>
<p>The budget must fail something, or it is a wish.</p>
<p><strong>In CI, on every pull request:</strong></p>
<div class="code"><span class="code-lang">yaml</span><pre><code class="lang-yaml">- name: bundle size
  run: npx size-limit          # fails if over the configured budget

- name: performance test
  run: k6 run --threshold 'http_req_duration{p(99)}&lt;500' load.js</code></pre></div>
<p><strong>Report the delta on the pull request.</strong> "This change adds 8 KB to the main bundle (142 KB → 150 KB, budget 170 KB)." Visible, in context, at the moment of decision.</p>
<p><strong>Allow overrides with a written reason.</strong> Not blocking forever — blocking until somebody says why. The record of overrides is itself useful; if you are overriding every week, the budget is wrong or the system is losing.</p>
<h2 id="the-budget-for-query-count">the budget for query count<a class="anchor" href="#the-budget-for-query-count" aria-label="link to this section">#</a></h2>
<p>The single most useful backend budget, and the least common.</p>
<p>Count database queries per request in tests. Assert a maximum.</p>
<div class="code"><span class="code-lang">python</span><pre><code class="lang-python">with assert_max_queries(10):
    client.get("/api/orders")</code></pre></div>
<p>This catches N+1 queries at the moment they are introduced, which is the only cheap time to catch them. An N+1 that reaches production is found weeks later by someone investigating a slow endpoint, and by then it is embedded in an ORM relationship that four other things depend on.</p>
<p>Almost every framework has a way to do this, and almost nobody does.</p>
<h2 id="what-happens-when-you-exceed-it">what happens when you exceed it<a class="anchor" href="#what-happens-when-you-exceed-it" aria-label="link to this section">#</a></h2>
<p>The conversation that a budget forces, in order:</p>
<ol><li><strong>Can we make the new thing cheaper?</strong> Lazy load it, defer it, make the query better.</li><li><strong>Can we remove something else?</strong> This is the valuable one. It surfaces the feature nobody uses and the library that is doing 5% of what it costs.</li><li><strong>Should the budget change?</strong> Sometimes yes, deliberately, with a reason recorded. A budget that never changes is a budget that will be ignored.</li><li><strong>Do we not ship this?</strong> Rare and it should be available.</li></ol>
<p>Any of those is better than the default, which is that the change lands and the system is permanently slower.</p>
<h2 id="the-thing-to-measure-first">the thing to measure first<a class="anchor" href="#the-thing-to-measure-first" aria-label="link to this section">#</a></h2>
<p>Before setting a budget, get the current numbers and the distribution. p50, p75, p95, p99. On real user hardware and real networks, not on a developer laptop on office wifi.</p>
<p>Then set the budget at roughly where you are, and ratchet it down over time rather than setting an aspirational number you fail immediately.</p>
<p>A budget you exceed on day one gets disabled on day two.</p>]]></content:encoded></item><item><title>The crawler tolls, one year on</title><link>https://readme.news/the-crawler-tolls-one-year-on/</link><guid isPermaLink="true">https://readme.news/the-crawler-tolls-one-year-on/</guid><pubDate>Wed, 01 Jul 2026 09:00:00 +0000</pubDate><description>Default blocking and pay-per-crawl changed who can read the web. An assessment of what actually happened.</description><content:encoded><![CDATA[<p>A year ago today a CDN sitting in front of a large fraction of the web flipped its default: AI crawlers blocked unless explicitly allowed, with a marketplace for charging per crawl.</p>
<p>Enough time has passed to say what actually happened rather than what everyone predicted.</p>
<h2 id="what-changed">what changed<a class="anchor" href="#what-changed" aria-label="link to this section">#</a></h2>
<p><strong>The norm inverted.</strong> Before, crawling was permitted by default and <code>robots.txt</code> was a request. Now, for a large share of the web, crawling is denied by default and access is a negotiation.</p>
<p>That is a genuine structural change to how the web works and it happened through one company's configuration default rather than through any standards process, legislation, or public deliberation.</p>
<p><strong>Licensing deals concentrated.</strong> Large AI companies negotiated bulk access with large publishers. That was always the likely outcome: the parties with lawyers and leverage made arrangements, and the arrangements are private.</p>
<p><strong>Small publishers got very little.</strong> The pay-per-crawl mechanism works, technically. The revenue for a site with modest traffic is negligible — the arithmetic never supported anything else. The publishers who most needed a new economic model got the one that pays least.</p>
<p><strong>Non-commercial crawling got harder.</strong> Academic researchers, archivists, and independent search projects have no licensing budget and no negotiating position. Carve-outs exist and are discretionary, which means the ability to study the web is now something you apply for.</p>
<p>That is the outcome I was most worried about and it is the one that materialized most clearly.</p>
<h2 id="what-did-not-change">what did not change<a class="anchor" href="#what-did-not-change" aria-label="link to this section">#</a></h2>
<p><strong>Training data supply.</strong> The frontier labs have enormous existing corpora, licensed sources, and synthetic generation. The marginal value of newly crawled web text was already declining. Restricting it did not create the leverage publishers hoped for.</p>
<p><strong>Traffic.</strong> Referral traffic from search to publishers continued its decline, driven by AI answers in search results, which is a completely separate mechanism from training crawlers and which blocking crawlers does nothing about.</p>
<p>This is the part that was most misunderstood at the time. The traffic problem and the training problem have different causes and blocking crawlers only addresses one of them — the one with less economic impact.</p>
<h2 id="the-thing-to-actually-take-from-it">the thing to actually take from it<a class="anchor" href="#the-thing-to-actually-take-from-it" aria-label="link to this section">#</a></h2>
<p><strong>Infrastructure defaults are policy.</strong> A configuration change at a chokepoint reshaped access to a large fraction of the web, with no process and no appeal.</p>
<p>That is not a criticism of the specific decision, which was popular and defensible. It is an observation about where power actually sits, and it generalizes: the entities that can change the web's behavior are the ones with concentration at a layer everyone depends on, and there are about five of them.</p>
<p><strong>For your own site</strong>, the decision remains yours and it is worth making deliberately rather than accepting a default:</p>
<ul><li><strong>Documentation sites</strong> frequently want to be in the training data. Being the thing the model knows about is worth more than the pageview you did not get.</li><li><strong>Original reporting and analysis</strong> has a stronger case for restriction.</li><li><strong>Anything you want found</strong> should still permit search crawlers, which are a different category and are frequently blocked by accident when people configure this.</li></ul>
<p>Check what you are actually blocking. A meaningful number of sites blocked their own search indexing in the first months of this and did not notice for weeks.</p>
<h2 id="the-unresolved-thing">the unresolved thing<a class="anchor" href="#the-unresolved-thing" aria-label="link to this section">#</a></h2>
<p>The web's economic model — publish freely, get traffic, monetize traffic — is breaking, and nothing has replaced it.</p>
<p>Crawler tolls are not the replacement; the arithmetic does not work at the scale of the actual web, where most content is made by people with no ability to negotiate anything.</p>
<p>Licensing deals are not the replacement either; they work for a few hundred large publishers and for nobody else.</p>
<p>I do not know what the replacement is. I am increasingly convinced that nobody does, and that the interval between the old model failing and a new one existing is going to be long and is going to be bad for the open web.</p>
<p>That is a genuinely pessimistic conclusion and I have not found a way around it in a year of thinking about it.</p>]]></content:encoded></item><item><title>I/O 2026 and the assistant that lives in everything</title><link>https://readme.news/io-2026-and-the-assistant-that-lives-in-everything/</link><guid isPermaLink="true">https://readme.news/io-2026-and-the-assistant-that-lives-in-everything/</guid><pubDate>Fri, 08 May 2026 09:00:00 +0000</pubDate><description>More model, more surfaces, and a search product that keeps changing what the web is for.</description><content:encoded><![CDATA[<p>Google's developer conference happened this week and the shape is consistent with where the company has been heading since 2024: a capable model, deployed everywhere they already have users, priced aggressively because they own the silicon.</p>
<h2 id="the-distribution-advantage-compounding">the distribution advantage, compounding<a class="anchor" href="#the-distribution-advantage-compounding" aria-label="link to this section">#</a></h2>
<p>The thing no competitor can replicate is that Google can ship a capability into products that billions of people already open daily, on launch day.</p>
<p>That is worth more than a benchmark lead and it is becoming more visible each year. A model that is marginally better but reaches users through a signup flow loses to a model that is marginally worse and is already in the search box.</p>
<p>The strategic implication for everyone else — including the other frontier labs — is that raw capability is not the competition anymore. Distribution, price, and integration are.</p>
<h2 id="the-search-question-again">the search question, again<a class="anchor" href="#the-search-question-again" aria-label="link to this section">#</a></h2>
<p>Every year this conference makes the same thing more true: informational queries are increasingly answered on the results page rather than by sending someone to a site.</p>
<p>For anyone who publishes on the web, the consequences are now well past theoretical:</p>
<ul><li><strong>Referral traffic to informational content keeps falling.</strong> This is measurable and it is not recovering.</li><li><strong>Your documentation is being summarized by a system you do not control</strong>, and users are acting on the summary.</li><li><strong>The <a class="xref" href="/why-your-tests-are-slow/" title="Why your tests are slow">feedback loop</a> is broken.</strong> You cannot see what people asked, what answer they got, or whether it was right.</li></ul>
<p>I do not have a satisfying answer. The mitigations available to an individual project are marginal: write documentation that is hard to summarize badly, keep a machine-readable <a class="xref" href="/the-unreasonable-effectiveness-of-a-changelog/" title="The unreasonable effectiveness of a changelog">changelog</a>, make <a class="xref" href="/error-messages-are-a-user-interface/" title="Error messages are a user interface">error messages</a> self-explanatory so they do not require a search at all.</p>
<p>The structural problem — that the economic model funding web content is being removed without a replacement — is not solvable by any individual publisher, and the people who could solve it have no incentive to.</p>
<h2 id="the-developer-surface">the developer surface<a class="anchor" href="#the-developer-surface" aria-label="link to this section">#</a></h2>
<p>The genuinely useful announcements, as always, are the boring ones:</p>
<p><strong>Model pricing and the cheap tier.</strong> The cost-per-capability at the small end continues falling, and Google's TPU position means they can price below what competitors renting accelerators can match. If you run high-volume inference, the arithmetic is worth redoing quarterly.</p>
<p><strong>Longer context, better retention.</strong> Incremental and real. The practical question is always the degradation curve, not the maximum, and that continues to improve.</p>
<p><strong>Agent tooling in the cloud.</strong> Same category everyone is building: runtimes, memory, identity, observability. Evaluate against a real problem rather than a demo.</p>
<h2 id="the-thing-to-actually-do">the thing to actually do<a class="anchor" href="#the-thing-to-actually-do" aria-label="link to this section">#</a></h2>
<p>The recommendation has not changed in two years and I will keep repeating it because it keeps being right:</p>
<p><strong>Have an eval set in your repository.</strong> Fifty examples from your real domain, with expected outputs, run against every candidate model.</p>
<p>Every conference like this produces a new model that is claimed to be better. With an eval harness, evaluating that claim for your workload takes an hour. Without one, it takes a week of impressions and you will get it wrong.</p>
<p>This is a day of setup that pays back on every model release, forever, and the number of teams that have done it remains surprisingly small.</p>
<h2 id="the-honest-summary">the honest summary<a class="anchor" href="#the-honest-summary" aria-label="link to this section">#</a></h2>
<p>A very good model, deployed extremely well, in a company with structural advantages that are getting stronger.</p>
<p>Whether that is good for the web is a separate question, and I keep arriving at the same uncomfortable answer.</p>]]></content:encoded></item><item><title>Accessibility is a testing problem</title><link>https://readme.news/accessibility-is-a-testing-problem/</link><guid isPermaLink="true">https://readme.news/accessibility-is-a-testing-problem/</guid><pubDate>Wed, 22 Apr 2026 09:00:00 +0000</pubDate><description>Most accessibility failures are mechanical and catchable. Treating it as a specialist concern is why it doesn&#x27;t get fixed.</description><content:encoded><![CDATA[<p>Accessibility gets treated as a specialist discipline requiring expert audits. Parts of it are. Most of what actually breaks is mechanical, catchable automatically, and would be fixed if it failed the build.</p>
<h2 id="what-automated-tooling-catches">what automated tooling catches<a class="anchor" href="#what-automated-tooling-catches" aria-label="link to this section">#</a></h2>
<p>Roughly a third to a half of real accessibility issues are detectable by a linter or a test:</p>
<ul><li>Images without alternative text.</li><li>Form inputs without associated labels.</li><li>Insufficient color contrast.</li><li>Missing document language.</li><li>Heading levels that skip.</li><li>Buttons and links with no accessible name.</li><li>ARIA attributes that are invalid or applied to the wrong role.</li><li>Interactive elements not reachable by keyboard.</li><li>Duplicate or missing landmark regions.</li></ul>
<p>Every one of those is a fixed rule with a fixed check, and every one of them is a real barrier for someone.</p>
<p>Getting the automatable third fixed is not the whole job and it is a much better place than most applications are.</p>
<h2 id="the-setup">the setup<a class="anchor" href="#the-setup" aria-label="link to this section">#</a></h2>
<div class="code"><span class="code-lang">js</span><pre><code class="lang-js">// component test
import { axe } from 'vitest-axe';

test('checkout form has no automatically detectable a11y violations', async () =&gt; {
  const { container } = render(&lt;CheckoutForm /&gt;);
  expect(await axe(container)).toHaveNoViolations();
});</code></pre></div>
<p>Plus a linter for the static cases:</p>
<div class="code"><span class="code-lang">json</span><pre><code class="lang-json">{ "extends": ["plugin:jsx-a11y/recommended"] }</code></pre></div>
<p>Plus a crawl of key pages in CI against the built site.</p>
<p>That is an afternoon of setup. It will find violations on day one — every codebase has them — so run it in report-only mode first, fix the backlog, then make it blocking. Making it blocking with a hundred existing violations gets it disabled within a week.</p>
<h2 id="what-tooling-cannot-catch">what tooling cannot catch<a class="anchor" href="#what-tooling-cannot-catch" aria-label="link to this section">#</a></h2>
<p>Being honest about the limits matters, because "we run axe" becomes a claim of compliance that it does not support.</p>
<p>**Whether the alt text is <em>good</em>.** <code>alt="image"</code> passes. It is useless. A screenshot of a chart needs a description of what the chart shows, not the word "chart."</p>
<p><strong>Whether the reading order makes sense.</strong> CSS can visually reorder content while the DOM order stays wrong, and a screen reader follows the DOM.</p>
<p><strong>Whether focus management works.</strong> Open a modal — where does focus go? Close it — does it return? Navigate to a new route — is focus reset and announced? These are the most common real-world failures in single-page applications and no automated check catches them.</p>
<p><strong>Whether dynamic content is announced.</strong> A live region that updates too often is worse than one that does not update at all.</p>
<p><strong>Whether it is actually usable.</strong> A page can pass every automated check and be completely impractical to navigate.</p>
<h2 id="the-manual-checks-worth-doing-routinely">the manual checks worth doing routinely<a class="anchor" href="#the-manual-checks-worth-doing-routinely" aria-label="link to this section">#</a></h2>
<p>Three, each a few minutes, on every significant feature:</p>
<p><strong>1. Unplug the mouse.</strong> Navigate the entire flow with <code>Tab</code>, <code>Shift+Tab</code>, <code>Enter</code>, <code>Space</code>, arrows, and <code>Escape</code>. Can you complete the task? Can you always see where focus is? Does focus ever get trapped, or jump somewhere unexpected?</p>
<p>This single test finds more real problems than any automated tool.</p>
<p><strong>2. Zoom to 400%.</strong> Browser zoom, at a 1280px viewport. Does content reflow, or does it require horizontal scrolling? Is anything cut off? This is a WCAG requirement and it is failed constantly.</p>
<p><strong>3. Turn on a screen reader for five minutes.</strong> VoiceOver on macOS (<code>Cmd+F5</code>), NVDA on Windows. You will be bad at it and that is fine — you are not evaluating your skill, you are listening to whether your page announces anything coherent.</p>
<p>Most developers who do this once are permanently changed by it, because the experience of hearing your own <a class="xref" href="/the-interface-is-the-product/" title="The interface is the product">interface</a> read aloud as "button, button, button, link, clickable" is clarifying.</p>
<h2 id="the-framing-that-gets-it-prioritized">the framing that gets it prioritized<a class="anchor" href="#the-framing-that-gets-it-prioritized" aria-label="link to this section">#</a></h2>
<p>Not "it is the right thing to do." That is true and it does not survive a prioritization meeting.</p>
<p><strong>It is a legal requirement</strong> in many jurisdictions, with an enforcement trend that is going up rather than down, and in the EU the European Accessibility Act now applies to a broad category of consumer-facing digital services.</p>
<p><strong>It is a larger market than most product managers assume.</strong> Roughly one in six people has a disability. Not all of those affect software use; enough do.</p>
<p><strong>It overlaps with quality generally.</strong> Semantic HTML, keyboard support, and clear focus states make interfaces better for everyone. Keyboard shortcuts are an accessibility feature that power users love.</p>
<p><strong>It is much cheaper to build in than to retrofit.</strong> An audit that finds two hundred issues in a shipped product is a quarter of remediation work. Catching them at the component level costs minutes.</p>
<h2 id="the-single-highest-value-practice">the single highest-value practice<a class="anchor" href="#the-single-highest-value-practice" aria-label="link to this section">#</a></h2>
<p>Use semantic HTML.</p>
<p>A <code>&lt;button&gt;</code> is focusable, keyboard-activatable, announced as a button, and works with every assistive technology, for free. A <code>&lt;div onclick&gt;</code> requires you to add a role, a tabindex, key handlers for Enter and Space, and focus styles — and you will get one of them wrong.</p>
<p>The overwhelming majority of accessibility problems in modern web applications come from reimplementing native elements badly. Use the element. It already works.</p>]]></content:encoded></item><item><title>Passkeys mostly won and nobody noticed</title><link>https://readme.news/passkeys-mostly-won-and-nobody-noticed/</link><guid isPermaLink="true">https://readme.news/passkeys-mostly-won-and-nobody-noticed/</guid><pubDate>Mon, 16 Mar 2026 09:00:00 +0000</pubDate><description>Phishing-resistant authentication is now the default on major platforms. The remaining problem is account recovery.</description><content:encoded><![CDATA[<p>Passkeys — WebAuthn credentials synced across a user's devices — are now the default or prominently offered sign-in method on most major consumer platforms. Adoption crossed the threshold where a developer building a new product should consider passwords the legacy option.</p>
<h2 id="why-they-actually-work">why they actually work<a class="anchor" href="#why-they-actually-work" aria-label="link to this section">#</a></h2>
<p>The security property that matters is not "no password to remember." It is <strong>origin binding</strong>.</p>
<p>A passkey is a keypair. The private key never leaves the authenticator. When you authenticate, the browser signs a challenge, and the signature includes the origin of the site requesting it.</p>
<p>Which means: a phishing site at <code>paypa1.com</code> cannot use a credential registered to <code>paypal.com</code>. Not "the user might notice." Cannot. The browser will not produce a signature for the wrong origin, and the attacker has nothing to replay.</p>
<p>That eliminates the single most successful attack class in consumer security. Password reuse, credential stuffing, real-time phishing proxies that capture TOTP codes — all defeated structurally rather than probabilistically.</p>
<h2 id="the-implementation-briefly">the implementation, briefly<a class="anchor" href="#the-implementation-briefly" aria-label="link to this section">#</a></h2>
<div class="code"><span class="code-lang">js</span><pre><code class="lang-js">// registration
const cred = await navigator.credentials.create({
  publicKey: {
    challenge: serverChallenge,          // from your server, random, single-use
    rp: { name: "README", id: "readme.news" },
    user: { id: userId, name: email, displayName: name },
    pubKeyCredParams: [{ type: "public-key", alg: -7 },   // ES256
                       { type: "public-key", alg: -257 }], // RS256
    authenticatorSelection: {
      residentKey: "preferred",
      userVerification: "preferred",
    },
  },
});</code></pre></div>
<p>Send the attestation to your server, verify it, store the credential ID and public key against the user.</p>
<p>Do not implement the verification yourself. Use a maintained library — the spec has enough subtleties that a hand-rolled verifier will have a bug, and the bug will be an authentication bypass.</p>
<h2 id="the-part-that-is-still-hard">the part that is still hard<a class="anchor" href="#the-part-that-is-still-hard" aria-label="link to this section">#</a></h2>
<p><strong>Account recovery.</strong> This is the unsolved problem and it is where every real deployment struggles.</p>
<p>If a user loses access to their passkeys, what happens? The options are all imperfect:</p>
<ul><li><strong>Fall back to email.</strong> Now your security is your email provider's security, and the phishing resistance you bought is gone for the recovery path.</li><li><strong>Backup codes.</strong> Users lose them or store them insecurely.</li><li><strong>Multiple passkeys on multiple devices.</strong> Good, and requires the user to have set that up before the loss.</li><li><strong>Identity verification.</strong> Expensive, invasive, and a social engineering target.</li></ul>
<p>The honest position: an authentication system is only as strong as its recovery path, and most passkey deployments have a recovery path that is substantially weaker than the primary path.</p>
<p>That is not an argument against passkeys — the primary path being strong still eliminates the bulk attacks. It is an argument for thinking about recovery as a first-class design problem rather than a fallback you bolt on.</p>
<h2 id="the-practical-guidance">the practical guidance<a class="anchor" href="#the-practical-guidance" aria-label="link to this section">#</a></h2>
<p><strong>Offer passkeys as an option, not a requirement</strong>, unless you control the device fleet. Users on older devices, shared computers, or unusual configurations need an alternative.</p>
<p><strong>Allow multiple passkeys per account.</strong> A user with a phone passkey and a hardware key has a recovery path that does not weaken the <a class="xref" href="/the-component-model-and-the-plugin-problem/" title="The component model and the plugin problem">security model</a>.</p>
<p><strong>Do not delete the password immediately.</strong> Let users add a passkey alongside, then prompt to remove the password later once they have used the passkey successfully a few times.</p>
<p><strong>Use conditional UI</strong> (autofill-style passkey prompts) rather than a separate button. The friction of "which sign-in method" is a real conversion cost and conditional UI removes it.</p>
<p><strong>For enterprise, device-bound keys.</strong> Synced passkeys follow the user's cloud account, which is convenient and is a different trust model. Hardware security keys with attestation are the right answer where the threat model warrants it.</p>
<h2 id="the-thing-that-took-ten-years">the thing that took ten years<a class="anchor" href="#the-thing-that-took-ten-years" aria-label="link to this section">#</a></h2>
<p>WebAuthn was standardized in 2019 and the underlying FIDO work started earlier. The technology was ready long before adoption happened.</p>
<p>What changed was not the protocol. It was that the platform vendors implemented syncing, which turned "a credential on one device" into "a credential you have," and that removed the objection that killed every previous attempt.</p>
<p>The lesson for anyone building security infrastructure: the cryptography is usually the easy part. The reason things do not get adopted is almost always a user experience problem, and solving it is real work that security people frequently consider beneath them.</p>]]></content:encoded></item><item><title>HTTP/3 and QUIC, five years in</title><link>https://readme.news/http3-and-quic-five-years-in/</link><guid isPermaLink="true">https://readme.news/http3-and-quic-five-years-in/</guid><pubDate>Fri, 13 Mar 2026 09:00:00 +0000</pubDate><description>It shipped, it works, most of the web uses it, and almost nobody understands what changed. A practical review.</description><content:encoded><![CDATA[<p>HTTP/3 is now the majority protocol for a large fraction of web traffic, supported by every major browser and CDN. Most developers have never thought about it, which is the correct outcome for a transport protocol.</p>
<p>It is still worth understanding what it actually changed, because a few of the consequences affect how you build.</p>
<h2 id="the-problem-it-solved">the problem it solved<a class="anchor" href="#the-problem-it-solved" aria-label="link to this section">#</a></h2>
<p>HTTP/2 introduced multiplexing: many logical streams over one TCP connection. That fixed head-of-line blocking at the HTTP layer.</p>
<p>It did not fix it at the TCP layer. TCP delivers bytes in order. If one packet is lost, everything behind it waits, including data for streams that were completely unaffected. On a lossy connection — mobile, congested wifi — HTTP/2 could be worse than HTTP/1.1 with six connections, because one loss stalled everything instead of one sixth of everything.</p>
<p>QUIC moves the transport to UDP and implements reliability, ordering, and congestion control per stream. A lost packet stalls only the stream it belonged to.</p>
<h2 id="what-else-came-with-it">what else came with it<a class="anchor" href="#what-else-came-with-it" aria-label="link to this section">#</a></h2>
<p><strong>Encryption is mandatory and integrated.</strong> TLS 1.3 is part of the protocol rather than a layer on top. The handshake is one round trip, or zero for a resumed connection.</p>
<p><strong>Connection migration.</strong> A QUIC connection is identified by a connection ID, not by the four-tuple of IP addresses and ports. Change networks — wifi to cellular — and the connection survives. Your download does not restart.</p>
<p>This is the feature users notice without knowing why. Walking out of a building while a video plays used to stall it.</p>
<p><strong>Better loss recovery.</strong> QUIC distinguishes between packet loss and reordering more accurately than TCP, and its acknowledgment format carries more information. Recovery is faster.</p>
<p><strong>Evolvability.</strong> TCP is implemented in kernels and middleboxes and cannot change, because the internet is full of devices that will drop anything unfamiliar. QUIC is in userspace and encrypted, so its internals are invisible to middleboxes and can actually be updated.</p>
<p>That last point is arguably the most important long-term consequence. Transport protocol ossification was a genuine crisis and QUIC is the <a class="xref" href="/platform-teams-that-dont-get-resented/" title="Platform teams that don&#x27;t get resented">escape hatch</a>.</p>
<h2 id="the-practical-consequences-for-you">the practical consequences for you<a class="anchor" href="#the-practical-consequences-for-you" aria-label="link to this section">#</a></h2>
<p><strong>Domain sharding is now actively harmful.</strong> Splitting assets across <code>static1.example.com</code> and <code>static2.example.com</code> was a workaround for HTTP/1.1's connection limit. Under HTTP/2 it was pointless. Under HTTP/3 it is worse than pointless, because each domain requires a separate connection with a separate handshake and separate congestion state.</p>
<p>One origin. If you still have sharding from a 2014 optimization guide, remove it.</p>
<p><strong>Concatenating and spriting are counterproductive.</strong> Same reasoning. Many small files multiplex fine and cache better individually.</p>
<p><strong>Priority matters and is under-configured.</strong> HTTP/3 has an extensible priority scheme. Most servers use defaults. If you have a page where certain resources are critical, priority hints (<code>fetchpriority</code>) are worth setting and are widely supported.</p>
<p><strong>UDP blocking is real but small.</strong> Some corporate networks block UDP/443. Clients fall back to HTTP/2 automatically, so this is a performance issue rather than a correctness one. Do not build anything that requires HTTP/3.</p>
<p><strong>Your observability may not see it.</strong> A lot of network monitoring tooling was built for TCP. Check whether your tools actually understand QUIC or are silently reporting nothing.</p>
<h2 id="the-parts-that-were-harder-than-expected">the parts that were harder than expected<a class="anchor" href="#the-parts-that-were-harder-than-expected" aria-label="link to this section">#</a></h2>
<p><strong>CPU cost.</strong> QUIC's userspace implementation and per-packet encryption use more CPU than kernel TCP. This has improved substantially with offload support and better implementations, and it is a real cost at high volume.</p>
<p><strong>Middlebox hostility.</strong> Some networks throttle or block UDP because it looks like something they should throttle. This is improving as QUIC becomes normal traffic.</p>
<p><strong>Debugging is harder.</strong> You cannot read a QUIC connection with tcpdump the way you could read HTTP/1.1. <code>qlog</code> and browser devtools help. The tooling is younger.</p>
<h2 id="the-assessment">the assessment<a class="anchor" href="#the-assessment" aria-label="link to this section">#</a></h2>
<p>For a user on a good connection, HTTP/3 is roughly a wash. For a user on a bad connection — mobile, congested, high latency, lossy — it is a substantial improvement, and those users are a large fraction of the world.</p>
<p>That is exactly the right kind of improvement: invisible to the people who were already fine, meaningful to the people who were not.</p>
<p>Enable it, remove your HTTP/1.1-era workarounds, and go back to not thinking about the transport layer.</p>]]></content:encoded></item><item><title>The 2026 web platform baseline</title><link>https://readme.news/the-2026-web-platform-baseline/</link><guid isPermaLink="true">https://readme.news/the-2026-web-platform-baseline/</guid><pubDate>Thu, 26 Feb 2026 09:00:00 +0000</pubDate><description>Container queries, `:has()`, view transitions, popover, anchor positioning. The polyfill era is over for real this time.</description><content:encoded><![CDATA[<p>The set of web platform features that work everywhere without a polyfill is substantially larger than most developers' mental model of it, because that mental model was formed during a decade when it was not true.</p>
<p>Here is what is now safe, and what it lets you delete.</p>
<h2 id="css-that-replaces-javascript">CSS that replaces JavaScript<a class="anchor" href="#css-that-replaces-javascript" aria-label="link to this section">#</a></h2>
<p><strong>Container queries.</strong> Style based on the container's size, not the viewport.</p>
<div class="code"><span class="code-lang">css</span><pre><code class="lang-css">.card-grid { container-type: inline-size; }

@container (min-width: 30rem) {
  .card { display: grid; grid-template-columns: 8rem 1fr; }
}</code></pre></div>
<p>This is the feature that makes genuinely reusable components possible. A component that adapts to the space it is given, wherever it is placed, without knowing about the page. Media queries never could do this and every component library worked around it with JavaScript resize observers.</p>
<p>Delete the resize observers.</p>
<p><strong><code>:has()</code>.</strong> The parent selector, requested for twenty years.</p>
<div class="code"><span class="code-lang">css</span><pre><code class="lang-css">/* a form field that has an invalid input */
.field:has(input:invalid) { border-color: var(--error); }

/* a card that contains an image gets different padding */
.card:has(img) { padding-top: 0; }

/* style a label based on its checkbox */
label:has(:checked) { font-weight: 600; }</code></pre></div>
<p>An enormous amount of state-syncing JavaScript exists purely to add a class to a parent based on a child's state. All of it can go.</p>
<p><strong>Nesting.</strong> Native CSS nesting works. For a lot of projects this removes the primary reason to run a preprocessor.</p>
<p><strong><code>:is()</code>, <code>:where()</code>, cascade layers.</strong> Specificity is finally manageable. <code>@layer</code> in particular lets you define an explicit cascade order rather than fighting specificity with <code>!important</code>.</p>
<h2 id="interaction-without-javascript">interaction without JavaScript<a class="anchor" href="#interaction-without-javascript" aria-label="link to this section">#</a></h2>
<p><strong>Popover API.</strong> Declarative popovers, tooltips, and menus with top-layer rendering, light-dismiss, and focus management handled by the browser.</p>
<div class="code"><span class="code-lang">html</span><pre><code class="lang-html">&lt;button popovertarget="menu"&gt;Options&lt;/button&gt;
&lt;div id="menu" popover&gt;...&lt;/div&gt;</code></pre></div>
<p>That is the whole implementation. No positioning library, no click-outside handler, no focus trap, no escape-key listener, no z-index arithmetic.</p>
<p><strong>Anchor positioning.</strong> Position an element relative to another element, in CSS, with automatic flipping when it would overflow the viewport.</p>
<div class="code"><span class="code-lang">css</span><pre><code class="lang-css">#menu {
  position-anchor: --trigger;
  top: anchor(bottom);
  left: anchor(left);
  position-try-fallbacks: flip-block, flip-inline;
}</code></pre></div>
<p>Combined with popover, this deletes an entire dependency category. Positioning libraries exist because CSS could not do this. Now it can.</p>
<p><strong>Dialog element.</strong> Modal semantics, focus management, backdrop, escape handling. Native.</p>
<p><strong>View transitions.</strong> Animate between states, including across page navigations in multi-page apps.</p>
<div class="code"><span class="code-lang">js</span><pre><code class="lang-js">document.startViewTransition(() =&gt; updateTheDOM());</code></pre></div>
<p>The cross-document version means a plain server-rendered site can have smooth transitions between pages. That removes one of the last genuine advantages of client-side routing, which was always a lot of complexity for an animation.</p>
<h2 id="the-things-to-still-be-careful-about">the things to still be careful about<a class="anchor" href="#the-things-to-still-be-careful-about" aria-label="link to this section">#</a></h2>
<p><strong>Feature detection over version detection.</strong> Always.</p>
<div class="code"><span class="code-lang">css</span><pre><code class="lang-css">@supports (anchor-name: --x) { /* enhanced */ }</code></pre></div>
<p><strong>Progressive enhancement still applies.</strong> A popover without JavaScript works. A view transition without support just does not transition, which is fine. Build so the absence degrades rather than breaks.</p>
<p><strong>Check your actual audience.</strong> Baseline "widely available" means roughly thirty months across all major browsers. If you support enterprise environments with pinned browser versions, your baseline is different and you should measure it rather than assume it.</p>
<h2 id="the-bigger-point">the bigger point<a class="anchor" href="#the-bigger-point" aria-label="link to this section">#</a></h2>
<p>The gap between "what the platform can do" and "what developers think it can do" is the widest it has ever been, and it is costing enormous amounts of unnecessary JavaScript.</p>
<p>A meaningful fraction of what frontend frameworks and utility libraries provide is now in the platform. Not all of it. More than most people realize.</p>
<p>Before you install a package to solve a UI problem, spend ten minutes checking whether CSS does it now. The answer has changed, recently, and probably more than once since you last looked.</p>]]></content:encoded></item><item><title>SQLite in the browser and the end of the API round trip</title><link>https://readme.news/sqlite-in-the-browser-and-the-end-of-the-api-round-trip/</link><guid isPermaLink="true">https://readme.news/sqlite-in-the-browser-and-the-end-of-the-api-round-trip/</guid><pubDate>Sat, 07 Feb 2026 09:00:00 +0000</pubDate><description>A real relational database in WebAssembly with durable storage. What it makes possible and what it breaks.</description><content:encoded><![CDATA[<p>SQLite compiled to <a class="xref" href="/the-component-model-and-the-plugin-problem/" title="The component model and the plugin problem">WebAssembly</a>, with an origin-private filesystem backing store, gives the browser a real relational database with real SQL, real transactions, and real durability.</p>
<p>That is a bigger change to web application architecture than it first appears.</p>
<h2 id="what-it-replaces">what it replaces<a class="anchor" href="#what-it-replaces" aria-label="link to this section">#</a></h2>
<p>The default architecture for a data-heavy web application is: the browser holds almost nothing, every view is a request, and the server does the querying.</p>
<p>That produces:</p>
<ul><li>A round trip for every interaction, at whatever latency the user's network has.</li><li>An API endpoint per view, because generic endpoints are inefficient and specific ones proliferate.</li><li>A loading state for everything, and the accompanying skeleton screens.</li><li>Nothing works offline.</li><li>Server cost proportional to reads, which are the overwhelming majority of traffic.</li></ul>
<p>If the client has the data, all of that changes. A view is a query against local storage. It returns in a millisecond. There is no loading state because there is no load.</p>
<h2 id="the-practical-setup">the practical setup<a class="anchor" href="#the-practical-setup" aria-label="link to this section">#</a></h2>
<div class="code"><span class="code-lang">js</span><pre><code class="lang-js">import { sqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm';

const db = await sqlite3Worker1Promiser.v2();
await db('open', { filename: 'file:app.sqlite3?vfs=opfs' });

await db('exec', {
  sql: `select id, title, updated_at from notes
        where project_id = ? order by updated_at desc limit 50`,
  bind: [projectId],
  rowMode: 'object',
});</code></pre></div>
<p>Run it in a worker. The synchronous access handle API — which is what makes durable storage fast — is only available off the main thread, and you did not want to block the main thread with database I/O anyway.</p>
<h2 id="the-design-questions-this-forces">the design questions this forces<a class="anchor" href="#the-design-questions-this-forces" aria-label="link to this section">#</a></h2>
<p><strong>How much data does a client get?</strong> All of it is fine for a personal notes app. It is catastrophic for a system where a user may see a tiny fraction of a large corpus, and it is a security problem if the partition is not enforced server-side.</p>
<p>The general answer is a <strong>working set</strong>: the user's own data, recently accessed shared data, and lazy loading for the rest. Deciding what is in the working set is the hard design problem and it is application-specific.</p>
<p><strong>How does it sync?</strong> This is where the complexity lives. Options range from "pull changes since a watermark and last-write-wins" to full CRDT-based merge. The right answer depends on whether concurrent edits to the same record are possible and what should happen when they are.</p>
<p>Start with the simplest thing that is correct for your data. Most applications have very little genuine concurrent editing and do not need CRDTs.</p>
<p><strong>What about permissions?</strong> The client has the data, so the client can read the data, so anything the client should not see must not be synced. Your sync layer has to understand your authorization model.</p>
<p>This is the constraint that rules out <a class="xref" href="/local-first-is-finally-practical/" title="Local-first is finally practical">local-first</a> for a lot of enterprise applications, and pretending otherwise is how you build a data leak.</p>
<p><strong>What about migrations?</strong> Users have databases on their machines in old schemas. You need versioned migrations that run on the client, and you need to handle a client that has been offline across four versions.</p>
<p>Build this on day one. It is very painful to retrofit.</p>
<h2 id="what-it-is-genuinely-good-for">what it is genuinely good for<a class="anchor" href="#what-it-is-genuinely-good-for" aria-label="link to this section">#</a></h2>
<ul><li>Applications with a personal working set: notes, tasks, editors, trackers, design tools, dashboards over a user's own data.</li><li>Anything where the user expects instant interaction.</li><li>Anything that should work on a plane.</li><li>Anything where read traffic dominates and server costs scale with it.</li></ul>
<h2 id="what-it-is-not-good-for">what it is not good for<a class="anchor" href="#what-it-is-not-good-for" aria-label="link to this section">#</a></h2>
<ul><li>Large shared datasets with narrow per-user views.</li><li>Anything requiring server-authoritative state.</li><li>Anything with fine-grained dynamic permissions.</li><li>Regulatory environments where data on client devices is restricted.</li></ul>
<h2 id="the-performance-note-that-surprises-people">the performance note that surprises people<a class="anchor" href="#the-performance-note-that-surprises-people" aria-label="link to this section">#</a></h2>
<p>For a dataset that fits — and "fits" is generous, tens of megabytes is unremarkable — local SQL queries are faster than a network request by roughly three orders of magnitude.</p>
<p>That is not an optimization. It is a different category of user experience, and users notice it immediately even if they cannot say why.</p>
<p>The application that responds instantly to every action feels like a tool. The one with a spinner on every click feels like a website. That distinction is worth more than most features.</p>]]></content:encoded></item><item><title>The component model and the plugin problem</title><link>https://readme.news/the-component-model-and-the-plugin-problem/</link><guid isPermaLink="true">https://readme.news/the-component-model-and-the-plugin-problem/</guid><pubDate>Sat, 24 Jan 2026 09:00:00 +0000</pubDate><description>WebAssembly&#x27;s most important feature isn&#x27;t running fast in a browser. It&#x27;s letting you run someone else&#x27;s code safely.</description><content:encoded><![CDATA[<p>WebAssembly's original pitch was speed in the browser. That pitch was approximately right and substantially less important than what it turned into.</p>
<p>The actual killer application is <strong>running untrusted code with a capability-based security model and a language-agnostic <a class="xref" href="/the-interface-is-the-product/" title="The interface is the product">interface</a></strong>, and the component model is the piece that makes it usable.</p>
<h2 id="the-problem-it-solves">the problem it solves<a class="anchor" href="#the-problem-it-solves" aria-label="link to this section">#</a></h2>
<p>Every extensible application faces the same question: how do I let third parties add functionality without letting them own my process?</p>
<p>The historical answers are all bad:</p>
<ul><li><strong>Dynamic libraries.</strong> Full process access. One bad plugin corrupts everything.</li><li><strong>An embedded scripting language.</strong> Better isolation, but you have committed everyone to Lua or JavaScript, and the FFI boundary is a source of both bugs and escapes.</li><li><strong>Subprocesses with IPC.</strong> Safe and slow, with serialization overhead on every call and a lot of plumbing.</li><li><strong>Containers.</strong> Very safe, very heavy. Startup in the tens or hundreds of milliseconds, memory overhead per instance.</li></ul>
<p>WebAssembly gives you: a sandbox with no ambient authority, sub-millisecond instantiation, memory measured in kilobytes per instance, near-native execution, and a compilation target for a dozen languages.</p>
<h2 id="what-the-component-model-adds">what the component model adds<a class="anchor" href="#what-the-component-model-adds" aria-label="link to this section">#</a></h2>
<p>Core WebAssembly can only pass integers and floats across its boundary. Everything else — strings, structs, lists, results — requires a hand-written serialization convention on both sides. Every host invented its own and none of them interoperated.</p>
<p>The component model defines a real interface <a class="xref" href="/type-systems-and-the-cost-of-being-right/" title="Type systems and the cost of being right">type system</a>, described in WIT (WebAssembly Interface Types):</p>
<div class="code"><span class="code-lang">wit</span><pre><code class="lang-wit">package readme:plugin@1.0.0;

interface transform {
    record document {
        title: string,
        body: string,
        tags: list&lt;string&gt;,
    }

    variant error {
        parse-failed(string),
        unsupported,
    }

    process: func(input: document) -&gt; result&lt;document, error&gt;;
}</code></pre></div>
<p>A component written in Rust exports that interface. A host written in Go imports it. Neither knows about the other's language. The types are checked at composition time.</p>
<p>That is the missing piece. It turns WebAssembly from "a fast sandbox you have to build a protocol on top of" into "a plugin system."</p>
<h2 id="the-capability-model">the capability model<a class="anchor" href="#the-capability-model" aria-label="link to this section">#</a></h2>
<p>A component gets nothing by default. No filesystem, no network, no clock, no random numbers, no environment variables. Every capability is explicitly granted by the host, and can be granted narrowly — this one directory, this one host, read-only.</p>
<p>That is a genuinely different security posture from every mainstream plugin system, and it is the right one. The failure mode of "a plugin can do anything the process can do" has produced a very long history of incidents.</p>
<p>WASI provides the standard interfaces for the capabilities a host chooses to grant, so components are portable across hosts that grant the same things.</p>
<h2 id="where-this-is-actually-being-used">where this is actually being used<a class="anchor" href="#where-this-is-actually-being-used" aria-label="link to this section">#</a></h2>
<ul><li><strong>Edge compute platforms</strong>, where cold start time is the product and containers are too slow.</li><li><strong>Database extensions</strong>, where you want user-defined functions without letting them segfault the database.</li><li><strong>Proxy and gateway filters</strong>, where the same filter should run in several different proxies.</li><li><strong>Plugin systems in developer tools</strong>, where users want to write extensions in their own language.</li></ul>
<h2 id="the-honest-state-of-it">the honest state of it<a class="anchor" href="#the-honest-state-of-it" aria-label="link to this section">#</a></h2>
<p><strong>Good:</strong> the core specification is stable, the toolchains for Rust and C are mature, the runtime implementations are production-quality.</p>
<p><strong>Mixed:</strong> language support varies enormously. Rust and C are excellent. Go works. Anything with a garbage collector and a large runtime — Python, Ruby, the JVM — produces large binaries and slower startup, though the WasmGC work improves this substantially for languages that adopt it.</p>
<p><strong>Still rough:</strong> debugging across the component boundary, async and streaming interfaces, and the tooling story for anyone who is not writing Rust.</p>
<h2 id="the-recommendation">the recommendation<a class="anchor" href="#the-recommendation" aria-label="link to this section">#</a></h2>
<p>If you are building anything extensible — an editor, a data pipeline, a platform, a game — evaluate this before you design your own plugin API. The security model alone justifies it, and the language-agnostic interface means your plugin ecosystem is not limited to people who like your language.</p>
<p>If you are not building anything extensible, this is not for you, and the browser-performance story is much less interesting than the marketing suggested in 2017.</p>]]></content:encoded></item><item><title>Cloudflare flips the default and starts charging crawlers</title><link>https://readme.news/cloudflare-flips-the-default-and-starts-charging-crawlers/</link><guid isPermaLink="true">https://readme.news/cloudflare-flips-the-default-and-starts-charging-crawlers/</guid><pubDate>Tue, 01 Jul 2025 09:00:00 +0000</pubDate><description>AI bots blocked unless allowed, plus a marketplace for per-crawl payment. A fifth of the web changes its robots policy at once.</description><content:encoded><![CDATA[<p>Cloudflare announced today that new domains on its network will block AI crawlers by default, and launched a pay-per-crawl marketplace letting site operators charge for access.</p>
<p>Cloudflare sits in front of roughly a fifth of the web. A default change at that position is not a product launch, it is a policy change for the internet.</p>
<h2 id="the-mechanism">the mechanism<a class="anchor" href="#the-mechanism" aria-label="link to this section">#</a></h2>
<p>Two pieces.</p>
<p><strong>Default blocking.</strong> New zones get AI crawler blocking on unless the operator opts out. Cloudflare maintains the bot classification — separating search crawlers, which drive traffic back, from training crawlers, which do not.</p>
<p><strong>Pay-per-crawl.</strong> A site sets a price. A crawler that wants the content gets an HTTP 402 Payment Required with terms. Cloudflare handles settlement.</p>
<p>HTTP 402 has been "reserved for future use" since 1997. It is genuinely funny that this is what activated it.</p>
<h2 id="why-the-old-system-failed">why the old system failed<a class="anchor" href="#why-the-old-system-failed" aria-label="link to this section">#</a></h2>
<p><code>robots.txt</code> is a request, not a control. It works because well-behaved crawlers choose to honor it, and that consensus held for thirty years because search engines had an incentive to be well-behaved — they needed publishers to not block them.</p>
<p>AI training crawlers have no such incentive. The content is valuable to them and the traffic they return is zero or nearly so. Multiple studies found training crawlers ignoring <code>robots.txt</code>, rotating user agents, and using residential proxy pools. Once a norm has no enforcement and no incentive, it stops being a norm.</p>
<p>Cloudflare's move replaces a request with a control. That is the actual innovation and it required no new technology at all — just someone at a chokepoint deciding to enforce.</p>
<h2 id="the-case-against">the case against<a class="anchor" href="#the-case-against" aria-label="link to this section">#</a></h2>
<p>Concentrating the ability to gate the web at one CDN is not obviously good, even if this specific use of the power is popular.</p>
<p>The precedent is: an infrastructure company can unilaterally change how content is accessed for a large fraction of the internet, and the mechanism generalizes to things other than AI crawlers. Cloudflare has been thoughtful and has taken public positions on not being an arbiter, and the concentration is still real.</p>
<p>There is also a smaller-player problem. Large AI companies can negotiate licensing deals directly. Researchers, startups, the Internet Archive, and academic crawlers cannot. A tollbooth is regressive: it is a rounding error for the incumbents and a barrier for everyone else. Cloudflare has carve-outs for some of these and the carve-outs are discretionary, which is the point.</p>
<h2 id="for-developers">for developers<a class="anchor" href="#for-developers" aria-label="link to this section">#</a></h2>
<p>Two practical items.</p>
<p><strong>If you run a site</strong>, decide deliberately. Blocking training crawlers is now the default; that may not be what you want. Documentation sites in particular may prefer to be in the training data, because being the thing the model knows about is worth more than the pageview you lost.</p>
<p><strong>If you build anything that crawls</strong>, expect 402s and expect your user agent to matter. Identify honestly, respect the directives, and set up billing if you need paid access. The era of scraping quietly is ending, and the enforcement is technical now rather than legal.</p>
<h2 id="the-bigger-shift">the bigger shift<a class="anchor" href="#the-bigger-shift" aria-label="link to this section">#</a></h2>
<p>The web's economic model was: publish freely, get traffic, monetize traffic. AI answers break the second step, which breaks the third, which will eventually break the first.</p>
<p>Pay-per-crawl is one proposed replacement. Licensing deals are another. Neither is obviously going to work at the scale of the actual web, where most content is made by people with no ability to negotiate anything.</p>
<p>What replaces it is genuinely unresolved, and 2025 is the year everybody stopped pretending otherwise.</p>]]></content:encoded></item><item><title>Chrome keeps third-party cookies after all</title><link>https://readme.news/chrome-keeps-third-party-cookies-after-all/</link><guid isPermaLink="true">https://readme.news/chrome-keeps-third-party-cookies-after-all/</guid><pubDate>Tue, 22 Apr 2025 09:00:00 +0000</pubDate><description>Five years, a Privacy Sandbox, a regulatory process, and the answer is: nothing changes.</description><content:encoded><![CDATA[<p>Google announced it will not ship the standalone prompt asking Chrome users whether to disable third-party cookies. The cookies stay. The Privacy Sandbox APIs remain available but are no longer the replacement for anything.</p>
<p>This ends a five-year process that reshaped the web advertising industry's roadmap and consumed an enormous amount of engineering attention across the entire ecosystem.</p>
<h2 id="the-timeline-compressed">the timeline, compressed<a class="anchor" href="#the-timeline-compressed" aria-label="link to this section">#</a></h2>
<ul><li><strong>2020</strong> — Google announces third-party cookies will be phased out within two years.</li><li><strong>2021</strong> — FLoC proposed. Universally criticized. Withdrawn.</li><li><strong>2022</strong> — Topics API replaces FLoC. Deadline slips.</li><li><strong>2023</strong> — Privacy Sandbox APIs ship. Deadline slips again.</li><li><strong>2024</strong> — UK CMA oversight formalized. Google announces a "user choice" prompt instead of deprecation. Deadline slips.</li><li><strong>2025</strong> — No prompt. Cookies stay.</li></ul>
<h2 id="why-it-failed">why it failed<a class="anchor" href="#why-it-failed" aria-label="link to this section">#</a></h2>
<p>Not primarily technical. The Privacy Sandbox APIs — Topics, Protected Audience, Attribution Reporting — were real engineering and some of the ideas were genuinely clever.</p>
<p>They failed because of an unresolvable structural conflict. Google needed the replacement to satisfy simultaneously:</p>
<ul><li><strong>Privacy advocates</strong>, who wanted cross-site tracking to stop.</li><li><strong>Publishers</strong>, who needed their ad revenue not to collapse.</li><li><strong>Advertisers</strong>, who needed measurement to still work.</li><li><strong>Regulators</strong>, who needed to be convinced Google was not using a privacy initiative to advantage its own first-party data — which it has enormous amounts of and which is unaffected by any third-party cookie change.</li></ul>
<p>That last constraint was the killer. Any third-party cookie deprecation strengthens Google's relative position, because Google has logged-in users everywhere and its competitors do not. The CMA was never going to wave that through, and Google was never going to ship something that hurt its own ad business.</p>
<p>Meanwhile Safari and Firefox blocked third-party cookies years ago and the web did not end. It just meant the tracking moved to fingerprinting, first-party data exchanges, server-side tagging, and CNAME cloaking — which are all worse for users because they are invisible and unblockable.</p>
<h2 id="the-lesson-worth-extracting">the lesson worth extracting<a class="anchor" href="#the-lesson-worth-extracting" aria-label="link to this section">#</a></h2>
<p>Privacy improvements that come from the browser with the dominant market share and an advertising business will always be structurally compromised. That is not a claim about anyone's intentions. It is a claim about incentives, and incentives are more predictable than intentions.</p>
<p>The improvements that actually shipped came from browsers without advertising businesses. Safari's ITP. Firefox's Total Cookie Protection. Those did not require a five-year multi-stakeholder process because there was no internal conflict to resolve.</p>
<h2 id="for-developers">for developers<a class="anchor" href="#for-developers" aria-label="link to this section">#</a></h2>
<p>Nothing you need to do. If you built for a cookieless future, that work is not wasted — Safari and Firefox users are already there, and they are a meaningful share of traffic for most sites.</p>
<p>If you are choosing an analytics stack, the interesting development of the last two years is that first-party server-side measurement is now easier and better than third-party client-side measurement for almost everything, cookies or not. Fewer requests, no ad blockers, better data, and you own it.</p>
<p>That transition was going to happen regardless of what Chrome decided.</p>]]></content:encoded></item><item><title>Chrome finishes off Manifest V2</title><link>https://readme.news/chrome-finishes-off-manifest-v2/</link><guid isPermaLink="true">https://readme.news/chrome-finishes-off-manifest-v2/</guid><pubDate>Fri, 07 Feb 2025 09:00:00 +0000</pubDate><description>The extension platform migration everyone fought about for six years reaches its end state.</description><content:encoded><![CDATA[<p>Chrome has begun disabling remaining Manifest V2 extensions on the stable channel. Users are seeing them turned off with a notice suggesting alternatives. Enterprise policy can delay this, but only for a while.</p>
<p>This has been announced, delayed, re-announced and re-delayed since 2019. It is now happening.</p>
<h2 id="the-technical-core-of-the-fight">the technical core of the fight<a class="anchor" href="#the-technical-core-of-the-fight" aria-label="link to this section">#</a></h2>
<p>MV2 gave extensions the <code>webRequest</code> API with blocking behavior: an extension could intercept a network request, run arbitrary JavaScript, and decide what to do. That is enormously powerful and enormously flexible.</p>
<p>MV3 replaces it with <code>declarativeNetRequest</code>: the extension registers rules ahead of time, and the browser evaluates them. The extension never sees the request.</p>
<p>Google's argument is performance, privacy and security: an extension that cannot see your requests cannot exfiltrate them, and rules evaluated in browser code are faster than a round trip into an extension's JavaScript context. All of that is genuinely true.</p>
<p>The counter-argument is capability. Content blocking that requires dynamic decisions — matching on response bodies, generating rules from observed traffic, handling sites that actively rotate their ad-serving domains — is harder or impossible under a declarative model. The rule count limits, though raised several times under pressure, are still limits.</p>
<h2 id="where-it-actually-lands">where it actually lands<a class="anchor" href="#where-it-actually-lands" aria-label="link to this section">#</a></h2>
<p>uBlock Origin's MV3 version, uBlock Origin Lite, works well for most users and is meaningfully less capable in the tail. Its author has been consistent and unusually clear-eyed about this: Lite is not Origin, it is a different product with a different capability envelope, and calling it a drop-in replacement would be dishonest.</p>
<p>For most people the difference will be invisible. For the people who notice, the difference is the whole point.</p>
<h2 id="the-part-that-is-actually-about-power">the part that is actually about power<a class="anchor" href="#the-part-that-is-actually-about-power" aria-label="link to this section">#</a></h2>
<p>It is possible to believe all of the following simultaneously:</p>
<ul><li>MV3's <a class="xref" href="/the-component-model-and-the-plugin-problem/" title="The component model and the plugin problem">security model</a> is a real improvement.</li><li>The declarative approach is the right architecture for most extensions.</li><li>A company whose revenue is advertising should not be the sole arbiter of what ad-blocking extensions can do in the browser used by most of the world.</li></ul>
<p>The third one is not a technical claim and cannot be resolved with a benchmark. It is a structural conflict of interest, and it will keep producing this exact argument every time Chrome makes a platform decision, regardless of the merits of that specific decision.</p>
<h2 id="what-to-do">what to do<a class="anchor" href="#what-to-do" aria-label="link to this section">#</a></h2>
<p>Firefox supports MV3 but kept blocking <code>webRequest</code>, which means full-featured content blockers still work there. Safari has its own model. If content blocking is load-bearing for you, that is now a browser choice, not an extension choice.</p>
<p>For extension developers: the migration is done, stop hedging, port it. The compatibility shims are worse than a real rewrite and you will maintain them forever.</p>
<p>For everyone else: this is a useful reminder that "the web platform" is a set of decisions made by a handful of engineering organizations, and the parts you rely on are only as durable as their continued interest in supporting them.</p>]]></content:encoded></item>
</channel>
</rss>
