<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Addy Osmani on Medium]]></title>
        <description><![CDATA[Stories by Addy Osmani on Medium]]></description>
        <link>https://medium.com/@addyosmani?source=rss-2508e4c7a8ec------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*DYJBahBIIojGtPfDRkmQcw.png</url>
            <title>Stories by Addy Osmani on Medium</title>
            <link>https://medium.com/@addyosmani?source=rss-2508e4c7a8ec------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 14:26:39 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@addyosmani/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Practical Loop Engineering]]></title>
            <link>https://medium.com/@addyosmani/practical-loop-engineering-ef207454e523?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/ef207454e523</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Sat, 22 Aug 2026 17:26:10 GMT</pubDate>
            <atom:updated>2026-08-22T17:26:10.414Z</atom:updated>
            <content:encoded><![CDATA[<h3>Goals, loops, and the discipline of not delegating your judgment</h3><p>The way that I typically work is I have anywhere between five and ten agents working at the same time in parallel. There are going to be some tasks that I’m very happy to delegate fully to agents, as long as I have a very clear idea of the stopping conditions and the constraints around them. And then there are going to be some tasks where I am going to want to keep a closer eye and code-review what the agent is doing.</p><p>Now within that, you’ve probably heard about <strong>loop engineering</strong>. I talked about it a couple of months ago when I wrote a big blog post about it.</p><blockquote><em>A loop is an autonomous, self-correcting feedback cycle where an AI agent repeatedly acts, tests its results and adjusts its approach until a specific goal is met</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ea2UI0DL9oRySZXUfCtd2A@2x.png" /></figure><p>There are now basically two core primitives you can think about. In Claude Code you have a <a href="https://code.claude.com/docs/en/goal"><strong>goal</strong></a><strong> primitive</strong>, which can drive a single bounded task forward until you’ve got a particular goal, like a measurable finish line that’s been met. And then <a href="https://code.claude.com/docs/en/scheduled-tasks"><strong>loop</strong></a> reruns on a timer or a fixed interval, so you can use it to kind of schedule changes.</p><h3>Before the primitives were primitives</h3><p>I remember back before we had primitives baked into Claude Code and Codex, loop engineering was heavily about setting up your own bash loop, a hand-rolled thing. That’s how I approached it. And you might remember earlier in the year, a number of us were playing around with the <a href="https://ghuntley.com/loop/">Ralph loop</a> by Geoff Huntley. We were experimenting, we were sharing our workflows, we were sharing what worked and what didn’t, but it was largely on some of our personal projects where, if we ran into a wall, it didn’t really have a big cost to us because it was on personal projects.</p><p>And as we’ve tried to see what patterns, what aspects of loop engineering are now a little bit more baked, I think we have a clearer idea of how you babysit it. I can now largely rely on the output of the primitives in Claude Code and Codex. We’ve come a little way. But at the same time, you need to be very diligent, because loop engineering where you kind of leave it sitting alone, and you haven’t really thought about whether the end goal or the constraints have been well defined, can leave you in a problematic state. This is why there’s nuance when deciding to use it for an evergreen codebase without users or as much historical complexity vs. say a brownfield bank codebase.</p><h3>How the Claude Code team frames loops</h3><p>The Claude Code team published their take on four kinds of loops and it lines up with how I use the primitives (<a href="https://x.com/ClaudeDevs/article/2074208949205881033">their write-up</a>). Before we get to it, here’s my quick summary:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*e1kExjG6nm0gjWQ1.png" /></figure><blockquote>On the Claude Code team, we define loops as agents repeating cycles of work until a stop condition is met. We categorize a few different types of loops based on: how they are triggered, how they are stopped, what Claude Code primitive is used, what type of task is most appropriate for each. Not all tasks require complex loops; start with the simplest solution and use these patterns selectively.</blockquote><blockquote>Every prompt you send starts a manual loop with you directing each turn. Claude gathers context, takes action, checks its work, repeats if needed, and responds. We call this the agentic loop. For example, ask Claude to create a like button. It reads your code, makes the edit, runs the tests, and hands back something it believes works. You then manually check the work, and write the next prompt.</blockquote><p>Their write-up walks each rung.</p><p><strong>On goal-based loops:</strong></p><blockquote>Sometimes, a single turn is not enough, especially for more complex tasks. Agents do better when they can iterate. You can extend how long Claude keeps iterating by defining what done looks like with /goal. When you define the success criteria, Claude doesn’t have to make a determination on what is “good enough” and end the loop early. Each time Claude tries to stop, an evaluator model checks your condition and sends it back to work until the goal is met or a number of turns you define is reached. This is why deterministic criteria, such as number of tests passed or clearing a certain score threshold, are so effective. For example: /goal get the homepage Lighthouse score to 90 or above, stop after 5 tries.</blockquote><p><strong>On time-based loops:</strong></p><blockquote>Some agentic work is recurring: the task stays the same and only the inputs change. For example, summarizing Slack messages every morning. Other work depends on external systems, and a simple way to interface with one is to check it on an interval and react to what changed. For example, a PR which may receive code reviews or fail CI. For these, you can trigger when Claude runs with /loop, which re-runs a prompt on an interval. For example: /loop 5m check my PR, address review comments, and fix failing CI. /loop runs on your computer, so if you turn it off, it stops. You can move the loop to the cloud by creating a routine with <a href="https://code.claude.com/docs/en/routines">/schedule</a>.</blockquote><p><strong>And on proactive loops, the top rung:</strong></p><blockquote>Triggered by: an event or schedule, with no human in real time. Stop criteria: each task exits when its goal is met. The routine itself runs until you turn it off. Best used for: recurring streams of well-defined work: bug reports, issue triage, migrations, dependency upgrades. Managed usage by: routing routines to smaller, faster models and using the most capable model for judgment calls.</blockquote><p>Their verification advice is worth lifting whole, because it moves the manual checking into something Claude applies itself:</p><pre>---<br>name: verify-frontend-change<br>description: Verify any UI change end-to-end before declaring it done.<br>---<br># Verifying frontend changes<br>Never report a UI change as complete based on a successful edit alone.<br>Verify it the way a human reviewer would:<br>1. Start the dev server and open the edited page in the browser.<br>2. Interact with the change directly. For a new control (button, input,<br>   toggle): click it, confirm the expected state change, and screenshot<br>   before/after.<br>3. Check the browser console: zero new errors or warnings.<br>4. Use the Chrome Devtools MCP, run a performance trace and audit<br>   Core Web Vitals.<br>If any step fails, fix the issue and rerun from step 1 - do not hand<br>back partially verified work.</pre><h3>Goal</h3><p>The way that I use goal is I use it for building any specific piece of work until it’s provably done. Like for example, you can use a goal to say: make sure that this experience loads in five seconds, keep going until it’s done. And it will continue to use an independent evaluation check to keep checking if the completion criteria has been met. What’s better is to be even more specific about what tooling is being used.</p><p>As for real goals that I’ve run to a number, there’s a few things that I’ve done. I’ve used goals for running through GitHub issues: review and close the last 10 issues, or review and move the last 10 issues forward, something like that. And that’s semi-open-ended, right? Or: let’s make this page load 50% faster. Sometimes that works well, sometimes it doesn’t, but it’s really about the experimentation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*i_5E6B8Qxm5h5gd3.png" /></figure><h3>Loop</h3><p>Loop is a little bit more of a scheduler, so it keeps an eye on something or repeatedly executes a pattern on some cadence. So think of it a little bit like a cron. It’s best for doing things like polling logs or monitoring external states. You could use it for potentially checking in. If there are repetitive tasks you find yourself doing on some cadence, loop is pretty good for that.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*H0wKzqi8-r-Nu6au.png" /></figure><h3>What I delegate, and what I watch</h3><p>For me, I use probably between five and ten agents every day. Very typically I’ll max out at about five concurrently. And some of those tasks might be ones that are a little bit safer. So maybe it’s, hey, I implemented this feature, go write the documentation for it. Or go double check that we have sufficient test coverage. Or something in that vein. If I’m working on a more complex problem, or something where I know that even if I’ve given it a good spec, or what I think was a good spec, and I’ve tried to give it some stopping conditions, there is still a reasonable chance it may not get everything right, I would try to watch it more closely. If the task involves anything just a little bit sensitive, whether it is I’ve given this access to a system, or whether the feature happens to touch authentication, or something related to security or finance, I’ll definitely be watching that closely.</p><p>Generally speaking, I do think we’re going to get to a place where folks are increasingly comfortable with delegation, as long as they’re able to have these clear ways of verifying that their goal or their stopping condition has been met. But you do still need to take a look at the code, at the thing that’s being generated, to make sure that it’s meeting your mark.</p><p>The other habit that matters here is not letting the agent that did the work decide the work is good. One sub-agent drafts the change. A separate one verifies it.</p><p>Sometimes an agent can be confident about something, and a verifying agent can catch things that they weren’t necessarily expecting. Like if it thinks that the baseline performance of an experience it’s generated is actually fine, and it’s only evaluating performance based on desktop, but you’re actually caring about the experience on mobile. That might mean the agent is being very confident about one dimension of the problem, but not the other.</p><p>And I learned this one the hard way. I was curious to figure out: is there something that we’re missing that we haven’t gotten direct feedback on from our users, either in an issue tracker or in a comment somewhere? And so I asked it to go and take a look at some of our competitors, and put together a list, and some PRs, not pushed, but some PRs locally, of what solving some of those gaps might look like. And I almost pushed some of those changes. But I didn’t actually look at them closely enough. I read through its research, but I didn’t look at the implementations closely enough. So I delegated the task, but I was close to delegating the judgment as well. Now, when I actually looked through the changes, what I realized is that it would introduce a lot of additional complexity for our users, for, I think personally, not all that much gain. And so I feel like you need to sometimes check yourself, that you are not delegating the taste and the judgment to your agent. You’re delegating the task, and then you are actually checking back that it’s meeting your bar.</p><p>The evaluator sitting behind goal is not that checker, by the way. It doesn’t look at the content to see if it’s good or bad in any way, shape, or form. All it does is examine the conversation transcript to see if the hard rules you specified have been met.</p><pre>/goal Refactor the data-fetching layer in Dashboard.tsx until Lighthouse performance score is &gt;= 92 and LCP is under 1.8s as shown by the Lighthouse CLI output. Do not change the public API of any hooks. Each turn must improve at least one reported metric; abort if two consecutive turns show no improvement. Stop after 10 turns.</pre><h3>The workflow I run every day</h3><p>One of the workflows that I do every day: I have a popular open source repository called <a href="https://github.com/addyosmani/agent-skills">Agent Skills</a>. We’ve got over 80,000 stars, and up until recently we were getting anywhere up to like 80 or 90 pull requests that we had to review a day. And so every day I would go and I would spend some time checking on this. Now with loop, what you can do is say: well, every 24 hours or every 12 hours, check the GitHub repository for any new open issues and provide a summary of their urgency, or provide a first pass review, or anything like that.</p><pre>/loop every 1h &quot;Check the GitHub repository for any new open issues. Provide a bulleted summary of their urgency.&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RLlbgTo5HyiFSK22.png" /></figure><h3>Combining loops and goals</h3><p>Now you can also sort of combine loops and goals. So you use loop to schedule a check, and then goal to solve the problem. So you can say something like: loop for every 24 hours, check GitHub for issues labeled bug. If one exists, use goal to implement a fix until all local tests pass and push the branch.</p><pre>/loop every 24h &quot;Check GitHub for issues labeled &#39;bug&#39;. If one exists, use /goal to implement a fix until all local tests pass and push the branch.&quot;</pre><p>But also keep in mind that goal has got some limitations around just how much you can sort of cram in there.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Ty_3pUV7V6Y2QcCG.png" /></figure><p>Their write-up also has a composed example that shows where this is all heading:</p><blockquote>The primitives above, along with other Claude Code features like <a href="https://code.claude.com/docs/en/auto-mode-config">auto mode</a> and dynamic workflows (research preview) can be composed into a loop for long-running work. For example, to handle incoming feedback, you can use: /schedule (research preview) to run a routine that checks for new reports, /goal to define what done looks like, and skills to document how to verify it. <a href="https://code.claude.com/docs/en/workflows">Dynamic workflows</a> to orchestrate agents that triage each report, fix it, and review the fix. Auto mode so the routine runs without stopping to ask for permission. Putting it together, a prompt could look like this: /schedule every hour: check the project-feedback channel for bug reports. /goal: don’t stop until every report found this run is triaged, actioned, and responded to. When fixing a bug, use a workflow to explore three solutions in parallel worktrees and have a judge adversarially review them.</blockquote><h3>What the triage system actually does</h3><p>Inside the PR triage, when I have loops and goals working together for me, what I end up having is a system that allows me to really continue accepting PRs and issues, being able to stay on top of what is coming on my plate every day, and especially being able to cross-reference. That’s been a really big deal for me. If I want to work on a particular goal of, hey, we’re going to be redoing this part of the system, and I need to make sure that any issues that happen to touch it are closed as a result of this rework, or we’re not stepping on anyone else’s toes, I can make sure that that’s well defined.</p><p>Scheduled tasks are really useful for regularly just reviewing new PRs and closing things that clearly don’t fit. One good stopping condition, for example: we have a set of contribution guidelines, and our contribution guidelines include things like, hey, we currently don’t accept translations.</p><p>And it’s not that we don’t care, it’s that they’re difficult for us to maintain, because we don’t speak all the languages often coming in. And so if we tell it, close any issues or close any PRs which happen to touch that aspect of our contribution guidelines, that’s something that can do really well when it’s on that schedule. And so it reduces the overall batch of things that we need to review.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VR9ssn3pjHAVJ7kt.png" /></figure><h3>What loops don’t buy you</h3><p>I’m often asked what loop engineering is not a great fit for. Generally speaking if you don’t have a clear idea of what the end-state/done/good means for your completion, it may not be the right pattern for your work. For example, a vague goal would be “keep going until this UI design is good”. What does that mean? Good to who? How is it being evaluated? Tasks that require human taste, subjective design, or open-ended creative exploration aren’t a good fit. When you have a pretty clear idea of the goal, I think loops are a good option to consider.</p><h3>The fine print</h3><p>One classic sign that you’ve got a loop spinning in place is the same command being tried over and over without any change in the result. Give the same command a third time with no change from the second and it’s probably time to stop.</p><p>One bit of fine print worth knowing. Recurring loops expire seven days after creation. I’d been telling people this was three days. It’s seven. And loops are session-scoped, so they stop when you start a new conversation — though resuming that session with — resume or — continue brings back any recurring task still inside its seven-day window. If you need something that outlives your session, /schedule runs it in the cloud.</p><p>If there’s a check you already run every morning by hand, that’s your first loop. Mine was the pull request pile.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IDCTWUQ1VK3jlwUIzFSVXQ@2x.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1_rdkGRD3KaUJ-go.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ef207454e523" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stop Using /init for AGENTS.md]]></title>
            <link>https://medium.com/@addyosmani/stop-using-init-for-agents-md-3086a333f380?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/3086a333f380</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ai-coding]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Thu, 19 Mar 2026 17:10:02 GMT</pubDate>
            <atom:updated>2026-03-19T17:10:02.926Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>TL;DR: A good mental model is to treat AGENTS.md as a living list of codebase smells you haven’t fixed yet, not a permanent configuration. Auto-generated AGENTS.md files hurt agent performance and inflate costs by 20%+ because they duplicate what agents can already discover. Human-written files help only when they contain non-discoverable information — tooling gotchas, non-obvious conventions, landmines. Every other line is noise.</strong></p><p>There’s a ritual that’s become almost universal among developers adopting AI coding agents. You set up a new repo, run /init, watch the agent scan your codebase, and get back a shiny AGENTS.md. It describes your directory structure, your tech stack, your testing conventions. You skim it, it looks reasonable, you commit it. You feel like you’ve done the responsible thing. Your agent is configured.</p><p>Two papers published in early 2026 suggest you might have just made your agent slower, more expensive, and no more accurate. And the implications go further than whether to include a codebase overview.</p><p>Beyond what to put in it, there’s a structural problem worth naming early: a single AGENTS.md at the root of your repo isn’t sufficient for any codebase of real complexity. What you actually need is a hierarchy of AGENTS.md files — placed at the relevant directory or module level — automatically maintained so that each agent gets context scoped precisely to the code it’s working in, rather than a monolithic file that conflates concerns across the entire project.</p><h3>What the research actually says</h3><p><a href="https://arxiv.org/abs/2601.20404">Lulla et al. (ICSE JAWs 2026)</a> ran a clean paired experiment: 124 real GitHub pull requests, executed with and without an AGENTS.md file, everything else held constant. Same task, same repo snapshot, same agent. They found that presence of an AGENTS.md file reduced median wall-clock runtime by 28.64% and output token consumption by 16.58%. Statistically significant. The agent got cheaper and faster.</p><p>That sounds like a clear win. Hold that thought.</p><p>A <a href="https://arxiv.org/abs/2602.11988">separate study out of ETH Zurich</a> tested four agents across SWE-bench and a custom benchmark of repos that already had developer-authored context files. Their finding cuts the other way: LLM-generated context files reduced task success by 2–3% while increasing cost by over 20%. Developer-written files improved success by about 4% — but also increased cost by up to 19%.</p><p>So which is it? Does AGENTS.md help or hurt?</p><p>Both, depending on what you put in it. And that’s actually the more interesting result.</p><p>The ETH Zurich paper discovered something that reframes the whole debate: when they stripped all documentation from the repos — READMEs, docs folders, markdown files — and <em>then</em> tested with LLM-generated context files, those files suddenly improved performance by 2.7%. The auto-generated content isn’t useless. It’s redundant. The agent could find all of it anyway by reading the repo. Hand it the same information twice and you’ve just added noise.</p><p>The Lulla paper, by contrast, used human-authored AGENTS.md files from repos where developers had actually been maintaining them. Real project-specific knowledge. Non-obvious tooling requirements. Actual gotchas. That’s the context that saves the agent time because it doesn’t have to infer what it can’t discover.</p><p>The question isn’t whether to have an AGENTS.md. The question is what earns a line in it.</p><h3>There’s a pink elephant problem</h3><p>There’s a subtler cost to AGENTS.md files that doesn’t show up in efficiency metrics: the anchoring effect.</p><p>If your AGENTS.md mentions that you use tRPC on the backend — even just as a passing note in an architecture overview — the model now has tRPC in context for every single prompt. If tRPC is only used in a handful of legacy endpoints and everything new runs on something else, you’ve just biased your agent toward the wrong pattern for the current codebase. You said it, it’s there, and LLMs don’t distinguish between “this is what we used to do” and “this is what you should do.”</p><p>This is the same reason prompt engineering wisdom has always said to be careful about what you put in your system prompt. Everything you add competes with the actual task. Research on LLM context more broadly shows a consistent pattern — more context often degrades performance, not just through cost but through diluted attention. Liu et al.’s “Lost in the Middle” result (2024) showed LLMs struggle with information placed in the middle of long contexts. Levy et al. showed that longer context degrades task performance even when the content is perfectly relevant. Every line in AGENTS.md is a line competing with the thing you actually asked the agent to do.</p><h3>The case against /init specifically</h3><p>Here’s the core problem with auto-generated context files. What does /init produce? Codebase overviews. Directory structure. Tech stack description. Module explanations. The ETH Zurich paper found that 100% of Sonnet 4.5’s auto-generated context files contained codebase overviews. 99% of GPT-5.2’s did the same.</p><p>These are precisely the things an agent can discover on its own by listing directories and reading your existing READMEs — which it does anyway, regardless of whether AGENTS.md exists. So you’ve added a file that the agent reads, then goes and confirms by reading your actual code, and now has to reconcile two sources of truth. More reasoning tokens. More steps. Same outcome, except slower and more expensive.</p><h3>What actually earns a line</h3><p>The ETH Zurich paper showed something concrete about what works. When a developer-written context file mentioned uv, agents used it 1.6 times per task on average. When it wasn’t mentioned, agents used it fewer than 0.01 times. The same pattern held for other repo-specific tools: 2.5 uses per task when mentioned, versus fewer than 0.05 when not.</p><p>uv versus pip is a perfect example of what belongs in AGENTS.md. It’s:</p><ul><li>Not discoverable from the codebase without inference</li><li>Operationally significant (it changes the commands the agent runs)</li><li>Impossible for the agent to guess correctly by convention</li></ul><p>Compare that to “this project uses a monorepo structure with packages in /packages” — something the agent finds in the first directory listing it runs. One of these earns a line. One of these is noise.</p><p>The practical filter is simple: can the agent discover this on its own by reading your code? If yes, delete it. Every line should represent information that isn’t already in the repo.</p><p>That means your AGENTS.md should look more like:</p><p>- Use `uv` for package management<br>- Always run tests with `–no-cache` or you’ll get false positives from the fixture setup<br>- The auth module uses a custom middleware pattern; do not refactor to standard Express middleware<br>- The `legacy/` directory is deprecated but imported by three production modules — don’t delete anything in it</p><p>And almost nothing else.</p><h3>The static file problem</h3><p>Even if you write a tight, non-redundant AGENTS.md, it has a structural weakness: it’s static, but tasks are dynamic.</p><p>Your file says “always run the test suite before committing.” The agent is doing a documentation change. It faithfully runs the full test suite. Tokens burned, minutes wasted, on an instruction that made sense for code changes but not for this one.</p><p>This isn’t a hypothetical failure mode. It’s baked into the architecture. A flat instruction set can’t condition on what kind of task is being run. An agent working on a CSS refactor doesn’t need your database migration warnings. One implementing a security fix probably should skip the performance optimization hints. A single monolithic file gets loaded the same way every time.</p><p><a href="https://arxiv.org/abs/2510.04618">The ACE framework</a> (Agentic Context Engineering, ICLR 2026) tackles this directly by treating context as an evolving playbook that adapts through a generator/reflector/curator pipeline, rather than a static file. On agent benchmarks, it outperformed static approaches by 12.3%. The architecture matters.</p><h3>The better architecture people aren’t building yet</h3><p>Several people in the AGENTS.md discourse have converged on roughly the same idea independently: the right structure isn’t a monolithic file, it’s a routing layer with focused context loaded on demand.</p><p>The structure would look something like:</p><p>Layer 1: Protocol file (what AGENTS.md should actually be) Not a codebase overview. Not a style guide. A routing document. Available personas and when to invoke them. Available skills and what task classes they cover. Available MCP connections and what they’re for. The minimum essential repo facts that the agent genuinely cannot discover — and nothing else.</p><p>Layer 2: Focused persona/skill files Each loaded selectively based on task type. A UX-focused agent working on component architecture loads different context than a backend agent debugging a data pipeline. Neither loads the other’s context. The total context for any given task stays bounded.</p><p>Layer 3: A maintenance subagent Whose only job is keeping the protocol file accurate as the codebase evolves. Because here’s the thing nobody talks about: documentation rots. The ETH Zurich study used freshly-generated context files and still found degraded performance. Real-world AGENTS.md files that haven’t been touched in six months — describing a dependency you’ve since replaced, a directory structure that’s changed — are going to be worse. Much worse.</p><p>None of the major coding agents expose the lifecycle hooks to make this architecture easy to build. You can approximate it with sub-agents and scoped context, but there’s no clean solution yet. That’s a tooling gap waiting to be filled.</p><h3>The automated optimization angle</h3><p>The most provocative finding in this space comes from <a href="https://arize.com/blog/claude-md-best-practices-learned-from-optimizing-claude-code-with-prompt-learning/">Arize AI’s prompt learning work</a>. Instead of manually writing CLAUDE.md instructions, they used an automated optimization loop — run the agent on training tasks, evaluate output, generate LLM feedback on why solutions failed, use meta-prompting to refine the instructions. Repeat.</p><p>Results: +5.19% accuracy improvement on a cross-repo test split. +10.87% on an in-repo split (training on past Django issues, testing on future ones).</p><p>What the optimizer discovered is the uncomfortable implication: what helps a human understand a codebase and what helps an LLM navigate it are often different things. An instruction that seems obviously useful to you — “this service uses the repository pattern” — might be noise to the model. And something the model actually needs — some particular import path disambiguation, some non-obvious file naming convention — might never occur to you to write down because you’ve internalized it.</p><p>The manual approach to AGENTS.md assumes that you know what the agent needs. The empirical evidence suggests you probably don’t. The optimizer figures out the delta between what you think matters and what actually moves the needle.</p><p>This doesn’t mean you should give up on writing context files — the automated optimization research is still early, and 5% is meaningful but not transformative. It means you should hold your intuitions about what to include loosely, and probably test them.</p><h3>The right mindset for AGENTS.md</h3><p>Think of AGENTS.md as a living document of friction you haven’t fixed yet.</p><p>Every line you add is a signal about something in your codebase that’s confusing enough to trip an AI agent — which means it’s probably confusing enough to trip a new human contributor too. The right response to that signal isn’t to grow the context file. It’s to fix the actual problem.</p><p>Agent keeps putting new utilities in the wrong directory? Maybe the directory structure is confusing and should be reorganized. Agent keeps reaching for a deprecated dependency? Maybe the import structure makes the wrong one too easy to grab. Agent forgetting to run type checks? Maybe the build pipeline should catch that automatically instead of relying on a prose instruction.</p><p>The AGENTS.md becomes a diagnostic tool rather than a permanent configuration. You add a line, you investigate why the agent keeps making this mistake, you fix the underlying thing, and then you can probably delete the line.</p><p>One technique worth trying: start your AGENTS.md nearly empty, and add a single instruction: “If you encounter something surprising or confusing in this project, flag it as a comment.” Most of the agent’s proposed additions aren’t things you want to keep permanently — they’re indicators of where the codebase is unclear. Fix those. Keep the file minimal.</p><h3>What the research doesn’t resolve</h3><p>Both papers have real limitations worth naming.</p><p>The Lulla paper doesn’t measure correctness at all — only efficiency. You can’t conclude from it that AGENTS.md improves output quality. The authors are clear about this: they performed a sanity check to confirm agents produced non-trivial output, but a full correctness evaluation is future work. Faster and cheaper is meaningful, but not if the code is wrong.</p><p>The ETH Zurich paper used fresh context files, which is ideal for experimental control but arguably tests a scenario most developers don’t actually live in. The real-world case is context files that have been in the repo for months and are partially out of date. That should perform worse than what the paper measured.</p><p>Neither paper tests the hierarchical, dynamically-loaded approach that multiple practitioners argue is the correct architecture. The “flat monolithic context file” is the only thing being evaluated. Whether a properly layered system with selective context loading performs differently is still an open empirical question.</p><p>And there’s the model trajectory problem: every few months, developers report needing less in their context files because the underlying models have gotten better at codebase navigation. Instructions that were essential six months ago become redundant as models improve. The AGENTS.md you write today might be pure overhead by next quarter.</p><h3>Practical takeaways</h3><p>Stop running /init. The auto-generated output is redundant with your existing documentation and adds overhead without benefit, unless your repo has genuinely zero documentation - in which case it’s marginally better than nothing.</p><p>Before adding any line to AGENTS.md, ask: can the agent find this by reading the code? If yes, don’t write it.</p><p>When an agent struggles with something repeatedly, treat it as a codebase problem before treating it as a context problem. Restructure the code. Add a linter rule. Improve the test coverage. Reach for AGENTS.md only after you’ve exhausted those options.</p><p>If you’re running agents at scale in CI/CD or automated review pipelines, the 15–20% cost overhead from context files compounds across thousands of runs. Calculate it before assuming the tradeoff is worth it.</p><p>Consider building a maintenance agent whose job is keeping the context file accurate rather than letting it rot.</p><p>And hold your intuitions about what the agent needs loosely. What seems obviously useful to you might be noise to the model. The empirical evidence suggests the gap between what we think agents need and what actually helps them is substantial — and probably not in the direction we’d expect.</p><p>The instinct to onboard your coding agent like a new hire — give it the office tour, explain the org chart, walk it through the architecture — comes from a reasonable place. But coding agents aren’t new hires. They can grep the entire codebase before you finish typing your prompt. What they need isn’t a map. They need to know where the landmines are.</p><p>And maybe, increasingly, they don’t even need that.</p><p>If you are interested in video, Theo’s take on this topic is excellent too.</p><iframe src="https://cdn.embedly.com/widgets/media.html?type=text%2Fhtml&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;schema=twitter&amp;url=https%3A//x.com/theo/status/2025900730847232409&amp;image=" width="500" height="281" frameborder="0" scrolling="no"><a href="https://medium.com/media/14fb40fd137d34cbda52a026e7eb1492/href">https://medium.com/media/14fb40fd137d34cbda52a026e7eb1492/href</a></iframe><p>as is Matt Pocock’s:</p><iframe src="https://cdn.embedly.com/widgets/media.html?type=text%2Fhtml&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;schema=twitter&amp;url=https%3A//x.com/mattpocockuk/status/2026230545605161119&amp;image=" width="500" height="281" frameborder="0" scrolling="no"><a href="https://medium.com/media/3815415581352ebfc0918b433826ab89/href">https://medium.com/media/3815415581352ebfc0918b433826ab89/href</a></iframe><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*O1hjiAhEhclLdE7v.jpg" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3086a333f380" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Comprehension Debt — the hidden cost of AI generated code.]]></title>
            <link>https://medium.com/@addyosmani/comprehension-debt-the-hidden-cost-of-ai-generated-code-285a25dac57e?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/285a25dac57e</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Sun, 15 Mar 2026 02:00:59 GMT</pubDate>
            <atom:updated>2026-03-15T02:00:59.214Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Comprehension debt is the hidden cost to human intelligence and memory resulting from excessive reliance on AI and automation. For engineers, it applies most to agentic engineering.</strong></p><p>There’s a cost that doesn’t show up in your velocity metrics when teams go deep on AI coding tools. Especially when its tedious to review all the code the AI generates. This cost accumulates steadily, and eventually it has to be paid - with interest. It’s called comprehension debt or <a href="https://www.media.mit.edu/publications/your-brain-on-chatgpt/">cognitive debt.</a></p><p>Comprehension debt is the <strong>growing gap</strong> between how much code exists in your system and <strong>how much of it any human being genuinely understands</strong>.</p><p>Unlike technical debt, which announces itself through mounting friction — slow builds, tangled dependencies, the creeping dread every time you touch that one module — comprehension debt breeds false confidence. The codebase looks clean. The tests are green. The reckoning arrives quietly, usually at the worst possible moment.</p><p><a href="https://margaretstorey.com/blog/2026/02/09/cognitive-debt/">Margaret-Anne Storey</a>’s describes a student team that hit this wall in week seven: they could no longer make simple changes <strong>without breaking something unexpected</strong>. The real problem wasn’t messy code. It was that no one on the team could explain why design decisions had been made or how different parts of the system were supposed to work together. The theory of the system had evaporated.</p><p>That’s comprehension debt compounding in real time.</p><p>I’ve read Hacker News threads tat captured engineers genuinely wrestling with the structural version of this problem — not the familiar optimism versus skepticism binary, but a field trying to figure out what rigor actually looks like when the bottleneck has moved.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7krhw0IFZzZIz_3X" /></figure><p>A recent Anthropic study titled “<a href="https://www.anthropic.com/research/AI-assistance-coding-skills">How AI Impacts Skill Formation</a>” highlighted the potential downsides of over-reliance on AI coding assistants. In a randomized controlled trial with 52 software engineers learning a newlibrary, participants who used AI assistance completed the task in roughly the same time as the control group but scored 17% lower on a follow-up comprehension quiz (50% vs. 67%). The largest declines occurred in debugging, with smaller but still significant drops in conceptual understanding and code reading. <strong>The researchers emphasize that passive delegation (“just make it work”) impairs skill development far more than active, question-driven use of AI.</strong> The full paper is available arXiv: <a href="https://arxiv.org/abs/2601.20245.">https://arxiv.org/abs/2601.20245.</a></p><h3>There is a speed asymmetry problem here</h3><p><strong>AI generates code far faster than humans can evaluate it. </strong>That sounds obvious, but the implications are easy to underestimate.</p><p><strong>When a developer on your team writes code, the human review process has always been a bottleneck — but a productive and educational one</strong>. Reading their PR forces comprehension. It surfaces hidden assumptions, catches design decisions that conflict with how the system was architected six months ago, and distributes knowledge about what the codebase actually does across the people responsible for maintaining it.</p><p>AI-generated code breaks that feedback loop. The volume is too high. The output is syntactically clean, often well-formatted, superficially correct — precisely the signals that historically triggered merge confidence. But surface correctness is not systemic correctness. The codebase looks healthy while comprehension quietly hollows out underneath it.</p><p>I read one engineer say that the bottleneck has always been a competent developer understanding the project. AI doesn’t change that constraint. It creates the illusion you’ve escaped it.</p><p>And the inversion is sharper than it looks. When code was expensive to produce, senior engineers could review faster than junior engineers could write. <strong>AI flips this: a junior engineer can now generate code faster than a senior engineer can critically audit it. </strong>The rate-limiting factor that kept review meaningful has been removed. <strong>What used to be a quality gate is now a throughput problem.</strong></p><h3>I love tests, but they aren’t a complete answer</h3><p>The instinct to lean harder on deterministic verification — unit tests, integration tests, static analysis, linters, formatters — is understandable. I do this a lot in projects heavily leaning on AI coding agents. Automate your way out of the review bottleneck. Let machines check machines.</p><p>This helps. It has a hard ceiling.</p><p>A test suite capable of covering all observable behavior would, in many cases, be more complex than the code it validates. Complexity you can’t reason about doesn’t provide safety though. And beneath that is a more fundamental problem: you can’t write a test for behavior you haven’t thought to specify.</p><p>Nobody writes a test asserting that dragged items shouldn’t turn completely transparent. Of course they didn’t. That possibility never occurred to them. That’s exactly the class of failure that slips through, not because the test suite was poorly written, but because no one thought to look there.</p><p>There’s also a specific failure mode worth naming. <strong>When an AI changes implementation behavior and updates hundreds of test cases to match the new behavior, the question shifts from “is this code correct?” to “were all those test changes necessary, and do I have enough coverage to catch what I’m not thinking about?” </strong>Tests cannot answer that question. Only comprehension can.</p><p>The data is starting to back this up. Research suggests that developers using AI for code generation delegation score below 40% on comprehension tests, while developers using AI for conceptual inquiry — asking questions, exploring tradeoffs — score above 65%. The tool doesn’t destroy understanding. How you use it does.</p><p>Tests are necessary. They are not sufficient.</p><h3>Lean on specs, but they’re also not the full story.</h3><p>A common proposed solution: write a detailed natural language spec first. Include it in the PR. Review the spec, not the code. Trust that the AI faithfully translated intent into implementation.</p><p>This is appealing in the same way Waterfall methodology was once appealing. Rigorously define the problem first, then execute. Clean separation of concerns.</p><p>The problem is that translating a spec to working code involves an enormous number of implicit decisions — edge cases, data structures, error handling, performance tradeoffs, interaction patterns — that no spec ever fully captures. <strong>Two engineers implementing the same spec will produce systems with many observable behavioral differences.</strong> Neither implementation is wrong. They’re just different. And many of those differences will eventually matter to users in ways nobody anticipated.</p><p>There’s another possibility with detailed specs worth calling out: a spec detailed enough to fully describe a program is more or less the program, just written in a non-executable language. The organizational cost of writing specs thorough enough to substitute for review may well exceed the productivity gains from using AI to execute them. And you still haven’t reviewed what was actually produced.</p><p>The deeper issue is that there is often no <em>correct </em>spec. Requirements emerge through building. Edge cases reveal themselves through use. The assumption that you can fully specify a non-trivial system before building it has been tested repeatedly and found wanting. AI doesn’t change this. It just adds a new layer of implicit decisions made without human deliberation.</p><h3>Learn from history</h3><p>Decades of managing software quality across distributed teams with varying context and communication bandwidth has produced real, tested practices. Those don’t evaporate because the team member is now a model.</p><p><strong>What changes with AI is cost (dramatically lower), speed (dramatically higher), and interpersonal management overhead (essentially zero). What doesn’t change is the need for someone with deep system context to maintain coherent understanding of what the codebase is actually doing and why.</strong></p><p>This is the uncomfortable redistribution that comprehension debt forces.</p><p>As AI volume goes up, the engineer who truly understands the system becomes more valuable, not less. The ability to look at a diff and immediately know which behaviors are load-bearing. To remember why that architectural decision got made under pressure eight months ago.</p><p>To tell the difference between a refactor that’s safe and one that’s quietly shifting something users depend on. That skill becomes the scarce resource the whole system depends on.</p><h3>There’s a bit of a measurement gap here too</h3><p><strong>The reason comprehension debt is so dangerous is that nothing in your current measurement system captures it.</strong></p><p>Velocity metrics look immaculate. DORA metrics hold steady. PR counts are up. Code coverage is green.</p><p>Performance calibration committees see velocity improvements. They cannot see comprehension deficits, because no artifact of how organizations measure output captures that dimension. The incentive structure optimizes correctly for what it measures. What it measures no longer captures what matters.</p><p>This is what makes comprehension debt more insidious than technical debt. Technical debt is usually a conscious tradeoff — you chose the shortcut, you know roughly where it lives, you can schedule the paydown. Comprehension debt accumulates invisibly, often without anyone making a deliberate decision to let it. It’s the aggregate of hundreds of reviews where the code looked fine and the tests were passing and there was another PR in the queue.</p><p>The organizational assumption that reviewed code is understood code no longer holds. Engineers approved code they didn’t fully understand, which now carries implicit endorsement. The liability has been distributed without anyone noticing.</p><h3>The regulation horizon is closer than it looks</h3><p>Every industry that moved too fast eventually attracted regulation. Tech has been unusually insulated from that dynamic, partly because software failures are often recoverable, and partly because the industry has moved faster than regulators could follow.</p><p>That window is closing. When AI-generated code is running in healthcare systems, financial infrastructure, and government services, “the AI wrote it and we didn’t fully review it” will not hold up in a post-incident report when lives or significant assets are at stake.</p><p>Teams building comprehension discipline now — treating genuine understanding, not just passing tests, as non-negotiable — will be better positioned when that reckoning arrives than teams that optimized purely for merge velocity.</p><h3>What comprehension debt actually demands</h3><p>The right question for now isn’t “how do we generate more code?” It’s “how do we actually understand more of what we’re shipping?” so we can make sure our users get a consistently high quality experience.</p><p>That reframe has practical consequences. It means being ruthlessly explicit about what a change is supposed to do before it’s written. It means treating verification not as an afterthought but as a structural constraint. It means maintaining the system-level mental model that lets you catch AI mistakes at architectural scale rather than line-by-line. And it means being honest about the difference between “the tests passed” and “I understand what this does and why.”</p><p><strong>Making code cheap to generate doesn’t make understanding cheap to skip. The comprehension work is the job.</strong></p><p>AI handles the translation. But someone still has to understand what was produced, why it was produced that way, and whether those implicit decisions were the right ones — or you’re just deferring a bill that will eventually come due in full.</p><p>You will pay for comprehension sooner or later. The debt accrues interest rapidly.</p><h4>Further reading with other perspectives:</h4><ul><li><a href="https://simonwillison.net/2026/Feb/15/cognitive-debt/">Simon on cognitive debt</a></li><li><a href="https://www.latent.space/p/reviews-dead">How to Kill the Code Review</a></li></ul><p><strong>Note: You can follow along more of my writing on my </strong><a href="https://addyo.substack.com"><strong>Substack</strong></a><strong> and </strong><a href="https://addyosmani.com"><strong>site</strong></a><strong>.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WPv0hU3C3EK5Fw15FrA7dg.jpeg" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=285a25dac57e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Announcing Full-Stack Web App Evaluation in Design Arena]]></title>
            <link>https://medium.com/@addyosmani/announcing-full-stack-web-app-evaluation-in-design-arena-899c9df7b054?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/899c9df7b054</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[ai-assisted-coding]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Mon, 16 Feb 2026 21:03:07 GMT</pubDate>
            <atom:updated>2026-02-16T22:09:09.020Z</atom:updated>
            <content:encoded><![CDATA[<p>By Grace Li (Design Arena) and Addy Osmani (Google Cloud AI)</p><p><strong>Most AI coding benchmarks for web focus <em>only</em> on front-end UI. It’s time to change that. We want you to be better set for building full-stack apps.</strong></p><p>Today, we’re announcing full-stack web app evaluation support in <a href="http://designarena.ai">Design Arena</a>. This is a pretty significant expansion that moves beyond frontend-only UI assessment to <strong>evaluate complete, production-ready applications</strong> with real backends and persistent data.</p><p>This has been a collaboration driven by a shared belief: <strong>developers deserve to know which AI coding tools can actually ship production applications, not just generate convincing-looking UI.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5TXp6ql2jmGVEUqFuVBGhw.png" /></figure><h3>The problems we’re solving</h3><p>Current AI models have become remarkably good at generating frontend code. Give them a prompt, and they’ll produce polished React components, responsive layouts, and interactive UI elements. <strong>The outputs <em>look</em> impressive.</strong></p><p>But here’s what we’ve observed across 1.6 million+ Design Arena users: <strong>there’s a meaningful gap between code that <em>looks like it works</em> and code that <em>actually works </em>in more than a toy app</strong>. A todo app with a beautiful interface but hardcoded data. A dashboard with stunning visualizations but no real API connections. A signup form that renders perfectly but doesn’t persist users to a database.</p><p>This isn’t a failure of the models of course. It reflects how we’ve been evaluating them. frontend-only benchmarks measure what models are optimized for. To understand full-stack capability, we need to evaluate full-stack behavior. Here’s a quick walkthrough:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FX1K5WBshcFQ%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DX1K5WBshcFQ&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FX1K5WBshcFQ%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/ea2bb1ea6f08423f106a8a0c30e32917/href">https://medium.com/media/ea2bb1ea6f08423f106a8a0c30e32917/href</a></iframe><h3>What does full-stack actually mean?</h3><p>For developers building production applications, full-stack means owning the complete flow:</p><p><strong>Authentication &amp; authorization</strong> — Not just a login form, but session management, token handling, role-based access, and middleware that actually protects routes.</p><p><strong>Data persistence</strong> — Real CRUD operations against real databases. Schemas that support the application’s needs. Queries that return actual data instead of mocked responses.</p><p><strong>Backend logic</strong> — API routes that validate input, handle errors gracefully, and execute business logic. Server-side actions that coordinate with the frontend.</p><p><strong>Deployment &amp; infrastructure</strong> — Code that runs in production rather than just in a sandbox. Environment variables, build pipelines, and containerized services.</p><p>When a user creates a profile, that data needs to persist. When they return tomorrow, their state should be there. When another user tries to access their data, authorization should block it. This is table stakes for production applications, and it’s what we’re now evaluating.</p><h3>Where this lives: Web App Arena</h3><p>Design Arena encompasses multiple evaluation categories — Website, UI Components, Game Dev, Data Viz, and more. Full-stack evaluation lives within <a href="https://www.designarena.ai/leaderboard/webapps"><strong>Web App Arena</strong></a>, the category specifically designed for functional web applications built with React.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s7aWiosXcUhCedqOG_S5Pg.png" /></figure><p>Web App Arena has always focused on applications that <em>do something</em> rather than just static pages or isolated components. Full-stack evaluation is a natural extension: we’re now testing whether those applications can do something with real data, real backends, and real persistence.</p><p><strong>Here’s a demo of Fullstack Arena in action creating a Notion note-taking like system, which would require fullstack capabilities like a database:</strong></p><iframe src="https://cdn.embedly.com/widgets/media.html?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Duy5GIX6cIU8&amp;type=text%2Fhtml&amp;schema=youtube&amp;display_name=YouTube&amp;src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fuy5GIX6cIU8%3Ffeature%3Doembed" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/c6ec95e35302fffcc502d43909483a62/href">https://medium.com/media/c6ec95e35302fffcc502d43909483a62/href</a></iframe><p>What’s interesting is that with this in place, you can see the difference between a poorly-implemented DB backend for the same “Create a Notion note-taking system” prompt as above.</p><p>Here’s the less good version, which creates a single monolithic table:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ofxfIQNqp2h4hXNFPp2b6Q.png" /></figure><p>and the better implementation (with granular tables and relationships):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u-47gVTlQJMdGkdpDmMXIA.png" /></figure><h3>The new architecture</h3><p>Full-stack evaluation deploys complete applications across three layers:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GLE9Tr2sGPjjf4HncaDtvQ.png" /></figure><p>The frontend connects to the backend via real API calls. The backend performs actual database operations against PostgreSQL (provided through Supabase). Data persists between sessions. This is the same architecture developers use in production — because that’s exactly what we’re testing.</p><h3>How we verify is a hybrid approach</h3><p>Evaluating full-stack applications is harder than evaluating UI. A button either renders or it doesn’t. But does the backend handle edge cases? Does the database schema support the required queries? Does the auth flow actually secure protected routes?</p><p>We use three complementary verification methods:</p><h3>1. Agentic testing</h3><p>An agent test runner generates and executes tests against deployed applications. This includes:</p><ul><li><strong>Browser-based UI flow tests</strong> — Simulating real user interactions</li><li><strong>Black-box backend tests</strong> — Verifying API behavior without access to source code</li><li><strong>End-to-end scenarios</strong> — Complete workflows from UI action to database state</li></ul><p>The agent doesn’t have access to the source code. It tests what a real user would experience.</p><h3>2. Deterministic verifiers</h3><p>Binary checks are in place that don’t require judgment:</p><ul><li>Does the database contain the expected records after a create operation?</li><li>Does the API return the correct status code for an unauthorized request?</li><li>Is the container healthy and responding?</li><li>Does the frontend successfully call the backend endpoints?</li></ul><p>These are pass/fail checks. No subjectivity, no ambiguity.</p><h3>3. Human preference</h3><p>Pairwise comparisons from the Design Arena community. Given two full-stack applications built from the same prompt, which one most closely matches the user’s intent? By shaping the product in a way where there are real stakes for the user, they only get to keep the winner, Design Arena incentivises user behavior to be in line with quality judgement.</p><p>Human judgment captures nuances that automated tests miss. Combined with deterministic verification, it provides a complete picture of capability.</p><h3>Why does this matter</h3><p>The AI coding landscape keeps evolving fast. Models are getting better. New tools ship weekly. Developers need reliable signals to understand which tools can handle which tasks.</p><p>Frontend generation has matured to the point where most leading models produce reasonable output. The differentiation is moving to full-stack capability — and that’s exactly where evaluation has been weakest.</p><p>By grounding evaluation in real deployment (Vercel, Docker, Supabase) and real user prompts (not synthetic benchmarks), we’re measuring what actually matters for production development.</p><h3>What are we measuring?</h3><p>Full-stack evaluation assesses:</p><ul><li><strong>Database integration</strong> — Schema design, query correctness, data persistence</li><li><strong>API implementation</strong> — Route handling, error responses, payload validation</li><li><strong>Authentication Flows</strong> — Login, signup, session management, protected routes</li><li><strong>Frontend-Backend Coordination</strong> — State management, data fetching, loading states</li><li><strong>Deployment Readiness</strong> — Does the complete application run in production?</li></ul><p>Each dimension surfaces different model capabilities. Some models excel at database schema design but struggle with auth flows. Others generate solid APIs but don’t wire them correctly to the frontend. Full-stack evaluation reveals these patterns.</p><h3>This is built on real developer behavior</h3><p>Like all Design Arena evaluations, full-stack assessment is grounded in actual user prompts rather than synthetic tasks designed to showcase capabilities. We’ve analyzed hundreds of thousands of prompts from developers building real projects. The evaluation tasks reflect what developers actually ask AI coding tools to do.</p><p>This matters because production requirements are messy. Users don’t ask for “a React component with proper prop typing.” They ask for “a dashboard that shows user activity from the last 30 days with the ability to export to CSV.” Full-stack evaluation tests whether models can handle the full scope of real requests.</p><h3>This is a snapshot but not the final word</h3><p>Model capabilities change. New versions ship. Fine-tuning improves specific behaviors. Design Arena’s evaluation represents a snapshot of current capability. This is pretty useful for informing tool selection today, but it is not a permanent ranking.</p><p>We update results regularly and encourage the community to surface failure modes we’ve missed. If you’ve deployed agent-generated code and found patterns that break, those insights make evaluation better for everyone.</p><h3>So what’s next?</h3><p>Full-stack evaluation is now live in Web App Arena. You can <a href="http://designarena.ai/?mode=max">test how different models</a> handle complete application development and see results on the Design Arena leaderboard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rVyjR9umV5squUZRzFrH8g.png" /></figure><p>We’re continuing to refine the verification approach based on observed failure patterns. As models improve, we’ll expand the complexity of evaluated scenarios. This can include multi-service architectures, more sophisticated auth patterns, complex database relationships.</p><p>The goal isn’t to declare winners. It’s to give developers accurate information about what AI coding tools can actually do right now, so they can make informed decisions about where these tools fit in their workflow. <strong>We hope the community finds this useful.</strong></p><p><em>Full-stack evaluation is available now at</em><a href="https://designarena.ai/"><em> designarena.ai</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=899c9df7b054" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My LLM coding workflow going into 2026]]></title>
            <link>https://medium.com/@addyosmani/my-llm-coding-workflow-going-into-2026-52fe1681325e?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/52fe1681325e</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Fri, 19 Dec 2025 20:16:14 GMT</pubDate>
            <atom:updated>2025-12-19T20:16:14.147Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>AI coding assistants became game-changers this year, but harnessing them effectively takes skill and structure.</strong> These tools dramatically increased what LLMs can do for real-world coding, and many developers (myself included) embraced them.</p><p>At Anthropic, for example, engineers adopted Claude Code so heavily that <a href="https://newsletter.pragmaticengineer.com/p/software-engineering-with-llms-in-2025#:~:text=,%E2%80%9D"><strong>today</strong></a><strong> ~90% of the code for Claude Code is written by Claude Code itself</strong>. Yet, using LLMs for programming is <em>not</em> a push-button magic experience — it’s “difficult and unintuitive” and getting great results requires learning new patterns. <a href="https://addyo.substack.com/p/critical-thinking-during-the-age">Critical thinking</a> remains key. Over a year of projects, I’ve converged on a workflow similar to what many experienced devs are discovering: treat the LLM as a powerful pair programmer that <strong>requires clear direction, context and oversight</strong> rather than autonomous judgment.</p><p>In this article, I’ll share how I plan, code, and collaborate with AI going into 2026, distilling tips and best practices from my experience and the community’s collective learning. It’s a more disciplined <strong>“AI-assisted engineering”</strong> approach — leveraging AI aggressively while <strong>staying proudly accountable for the software produced</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*66JE-vvcgLx-SNS5oFLqgw.png" /></figure><h3>Start with a clear plan (specs before code)</h3><p><strong>Don’t just throw wishes at the LLM — begin by defining the problem and planning a solution.</strong></p><p>One common mistake is diving straight into code generation with a vague prompt. In my workflow, and in many others’, the first step is <strong>brainstorming a detailed specification</strong> <em>with</em> the AI, then outlining a step-by-step plan, <em>before</em> writing any actual code. For a new project, I’ll describe the idea and ask the LLM to <strong>iteratively ask me questions</strong> until we’ve fleshed out requirements and edge cases. By the end, we compile this into a comprehensive <strong>spec.md</strong> — containing requirements, architecture decisions, data models, and even a testing strategy. This spec forms the foundation for development.</p><p>Next, I feed the spec into a reasoning-capable model and prompt it to <strong>generate a project plan</strong>: break the implementation into logical, bite-sized tasks or milestones. The AI essentially helps me do a mini “design doc” or project plan. I often iterate on this plan — editing and asking the AI to critique or refine it — until it’s coherent and complete. <em>Only then</em> do I proceed to coding. This upfront investment might feel slow, but it pays off enormously. As Les Orchard <a href="https://blog.lmorchard.com/2025/06/07/semi-automatic-coding/#:~:text=Accidental%20waterfall%20">put it</a>, it’s like doing a <strong>“waterfall in 15 minutes”</strong> — a rapid structured planning phase that makes the subsequent coding much smoother.</p><p>Having a clear spec and plan means when we unleash the codegen, both the human and the LLM know exactly what we’re building and why. In short, <strong>planning first</strong> forces you and the AI onto the same page and prevents wasted cycles. It’s a step many people are tempted to skip, but experienced LLM developers now treat a robust spec/plan as the cornerstone of the workflow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UzJIg803mVdA97Vd.png" /></figure><h3>Break work into small, iterative chunks</h3><p><strong>Scope management is everything — feed the LLM manageable tasks, not the whole codebase at once.</strong></p><p>A crucial lesson I’ve learned is to avoid asking the AI for large, monolithic outputs. Instead, we <strong>break the project into iterative steps or tickets</strong> and tackle them <a href="https://blog.fsck.com/2025/10/05/how-im-using-coding-agents-in-september-2025/#:~:text=please%20write%20out%20this%20plan%2C,in%20full%20detail%2C%20into%20docs%2Fplans">one by one</a>. This mirrors good software engineering practice, but it’s even more important with AI in the loop. LLMs do best when given focused prompts: implement one function, fix one bug, add one feature at a time. For example, after planning, I will prompt the codegen model: <em>“Okay, let’s implement Step 1 from the plan”</em>. We code that, test it, then move to Step 2, and so on. Each chunk is small enough that the AI can handle it within context and you can understand the code it produces.</p><p>This approach guards against the model going off the rails. If you ask for too much in one go, it’s likely to get confused or produce a <strong>“jumbled mess”</strong> that’s hard to untangle. Developers <a href="https://albertofortin.com/writing/coding-with-ai#:~:text=No%20consistency%2C%20no%20overarching%20plan,the%20other%209%20were%20doing">report</a> that when they tried to have an LLM generate huge swaths of an app, they ended up with inconsistency and duplication — “like 10 devs worked on it without talking to each other,” one said. I’ve felt that pain; the fix is to <strong>stop, back up, and split the problem into smaller pieces</strong>. Each iteration, we carry forward the context of what’s been built and incrementally add to it. This also fits nicely with a <strong>test-driven development (TDD)</strong> approach — we can write or generate tests for each piece as we go (more on testing soon).</p><p>Several coding-agent tools now explicitly support this chunked workflow. For instance, I often generate a structured <strong>“prompt plan”</strong> file that contains a sequence of prompts for each task, so that tools like Cursor can execute them one by one. The key point is to <strong>avoid huge leaps</strong>. By iterating in small loops, we greatly reduce the chance of catastrophic errors and we can course-correct quickly. LLMs excel at quick, contained tasks — use that to your advantage.</p><h3>Provide extensive context and guidance</h3><p><strong>LLMs are only as good as the context you provide — <em>show them</em> the relevant code, docs, and constraints.</strong></p><p>When working on a codebase, I make sure to <strong>feed the AI all the information it needs</strong> to perform well. That includes the code it should modify or refer to, the project’s technical constraints, and any known pitfalls or preferred approaches. Modern tools help with this: for example, Anthropic’s Claude can import an entire GitHub repo into its context in “Projects” mode, and IDE assistants like Cursor or Copilot auto-include open files in the prompt. But I often go further — I will either use an MCP like <a href="https://context7.com/">Context7</a> or manually copy important pieces of the codebase or API docs into the conversation if I suspect the model doesn’t have them.</p><p>Expert LLM users emphasize this “context packing” step. For example, doing a <strong>“brain dump”</strong> of everything the model should know before coding, including: high-level goals and invariants, examples of good solutions, and warnings about approaches to avoid. If I’m asking an AI to implement a tricky solution, I might tell it which naive solutions are too slow, or provide a reference implementation from elsewhere. If I’m using a niche library or a brand-new API, I’ll paste in the official docs or README so the AI isn’t flying blind. All of this upfront context dramatically improves the quality of its output, because the model isn’t guessing — it has the facts and constraints in front of it.</p><p>There are now utilities to automate context packaging. I’ve experimented with tools like <a href="https://gitingest.com/"><strong>gitingest</strong></a> or <a href="https://github.com/abinthomasonline/repo2txt"><strong>repo2txt</strong></a>, which essentially <strong>“dump” the relevant parts of your codebase into a text file for the LLM to read</strong>. These can be a lifesaver when dealing with a large project — you generate an output.txt bundle of key source files and let the model ingest that. The principle is: <strong>don’t make the AI operate on partial information</strong>. If a bug fix requires understanding four different modules, show it those four modules. Yes, we must watch token limits, but current frontier models have pretty huge context windows (tens of thousands of tokens). Use them wisely. I often selectively include just the portions of code relevant to the task at hand, and explicitly tell the AI what <em>not</em> to focus on if something is out of scope (to save tokens).</p><p>I think <a href="https://github.com/anthropics/skills"><strong>Claude Skills</strong></a> have potential because they turn what used to be fragile repeated prompting into something <strong>durable and reusable</strong> by packaging instructions, scripts, and domain specific expertise into modular capabilities that tools can automatically apply when a request matches the Skill. This means you get more reliable and context aware results than a generic prompt ever could and you move away from one off interactions toward workflows that encode repeatable procedures and team knowledge for tasks in a consistent way. A number of community-curated <a href="https://www.x-cmd.com/skill/">Skills collections</a> exist, but one of my favorite examples is the <a href="https://x.com/trq212/status/1989061937590837678">frontend-design</a> skill which can “end” the purple design aesthetic prevalent in LLM generated UIs. Until more tools support Skills officially, <a href="https://github.com/intellectronica/skillz">workarounds</a> exist.</p><p>Finally, <strong>guide the AI with comments and rules inside the prompt</strong>. I might precede a code snippet with: “Here is the current implementation of X. We need to extend it to do Y, but be careful not to break Z.” These little hints go a long way. LLMs are <strong>literalists</strong> — they’ll follow instructions, so give them detailed, contextual instructions. By proactively providing context and guidance, we minimize hallucinations and off-base suggestions and get code that fits our project’s needs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*j6iibIrXoljlMbOP.png" /></figure><h3>Choose the right model (and use multiple when needed)</h3><p><strong>Not all coding LLMs are equal — pick your tool with intention, and don’t be afraid to swap models mid-stream.</strong></p><p>In 2025 we’ve been spoiled with a variety of capable code-focused LLMs. Part of my workflow is <strong>choosing the model or service best suited to each task</strong>. Sometimes it can be valuable to even try two or more LLMs in parallel to cross-check how they might approach the same problem differently.</p><p>Each model has its own “personality”. The key is: <strong>if one model gets stuck or gives mediocre outputs, try another.</strong> I’ve literally copied the same prompt from one chat into another service to see if it can handle it better. This “<a href="https://blog.lmorchard.com/2025/06/07/semi-automatic-coding/#:~:text=I%20bounced%20between%20Claude%20Sonnet,Each%20had%20its%20own%20personality">model musical chairs</a>” can rescue you when you hit a model’s blind spot.</p><p>Also, make sure you’re using <em>the best version</em> available. If you can, use the newest “pro” tier models — because quality matters. And yes, it often means paying for access, but the productivity gains can justify it. Ultimately, pick the AI pair programmer whose <strong>“vibe” meshes with you</strong>. I know folks who prefer one model simply because they like how its responses <em>feel</em>. That’s valid — when you’re essentially in a constant dialogue with an AI, the UX and tone make a difference.</p><p>Personally I gravitate towards Gemini for a lot of coding work these days because the interaction feels more natural and it often understands my requests on the first try. But I will not hesitate to switch to another model if needed; sometimes a second opinion helps the solution emerge. In summary: <strong>use the best tool for the job, and remember you have an arsenal of AIs at your disposal.</strong></p><h3>Leverage AI coding across the lifecycle</h3><p><strong>Supercharge your workflow with coding-specific AI help across the SDLC.</strong></p><p>On the command-line, new AI agents emerged. <strong>Claude Code, OpenAI’s Codex CLI</strong> and <strong>Google’s Gemini CLI</strong> are CLI tools where you can chat with them directly in your project directory — they can read files, run tests, and even multi-step fix issues. I’ve used Google’s <strong>Jules </strong>and GitHub’s <strong>Copilot Agent</strong> as well — these are <strong>asynchronous coding agents</strong> that actually clone your repo into a cloud VM and work on tasks in the background (writing tests, fixing bugs, then opening a PR for you). It’s a bit eerie to witness: you issue a command like “refactor the payment module for X” and a little while later you get a pull request with code changes and passing tests. We are truly living in the future. You can read more about this in <a href="https://addyo.substack.com/p/conductors-to-orchestrators-the-future">conductors to orchestrators</a>.</p><p>That said, <strong>these tools are not infallible, and you must understand their limits</strong>. They accelerate the mechanical parts of coding — generating boilerplate, applying repetitive changes, running tests automatically — but they still benefit greatly from your guidance. For instance, when I use an agent like Claude or Copilot to implement something, I often supply it with the plan or to-do list from earlier steps so it knows the exact sequence of tasks. If the agent supports it, I’ll load up my spec.md or plan.md in the context before telling it to execute. This keeps it on track.</p><p><strong>We’re not at the stage of letting an AI agent code an entire feature unattended</strong> and expecting perfect results. Instead, I use these tools in a supervised way: I’ll let them generate and even run code, but I keep an eye on each step, ready to step in when something looks off. There are also orchestration tools like <strong>Conductor</strong> that let you run multiple agents in parallel on different tasks (essentially a way to scale up AI help) — some engineers are experimenting with running 3–4 agents at once on separate features. I’ve dabbled in this “massively parallel” approach; it’s surprisingly effective at getting a lot done quickly, but it’s also mentally taxing to monitor multiple AI threads! For most cases, I stick to one main agent at a time and maybe a secondary one for reviews (discussed below).</p><p>Just remember these are power tools — you still control the trigger and guide the outcome.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2dj5RFoWgD-o02h-.png" /></figure><p><em>A full overview of where AI can improve the developer experience. This spans design, inner, submit, and outer loops — highlighting every point where AI can meaningfully reduce toil.</em></p><h3>Keep a human in the loop — verify, test, and review everything</h3><p><strong>AI will happily produce plausible-looking code, but <em>you</em> are responsible for quality — always review and test thoroughly.</strong> One of my cardinal rules is never to blindly trust an LLM’s output. As Simon Willison aptly <a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#:~:text=Instead%2C%20use%20them%20to%20augment,on%20tedious%20tasks%20without%20complaint">says</a>, think of an LLM pair programmer as <strong>“over-confident and prone to mistakes”</strong>. It writes code with complete conviction — including bugs or nonsense — and won’t tell you something is wrong unless you catch it. So I treat every AI-generated snippet as if it came from a junior developer: I read through the code, run it, and test it as needed. <strong>You absolutely have to test what it writes</strong> — run those unit tests, or manually exercise the feature, to ensure it does what it claims. Read more about this in <a href="https://addyo.substack.com/p/vibe-coding-is-not-an-excuse-for">vibe coding is not an excuse for low-quality work</a>.</p><p>In fact, I weave testing into the workflow itself. My earlier planning stage often includes generating a list of tests or a testing plan for each step. If I’m using a tool like Claude Code, I’ll instruct it to run the test suite after implementing a task, and have it debug failures if any occur. This kind of tight feedback loop (write code → run tests → fix) is something AI excels at <em>as long as the tests exist</em>. It’s no surprise that those who get the most out of coding agents tend to be those with strong testing practices. An agent like Claude can “fly” through a project with a good test suite as safety net. Without tests, the agent might blithely assume everything is fine (“sure, all good!”) when in reality it’s broken several things. So, <strong>invest in tests</strong> — it amplifies the AI’s usefulness and confidence in the result.</p><p>Even beyond automated tests, <strong>do code reviews — both manual and AI-assisted</strong>. I routinely pause and review the code that’s been generated so far, line by line. Sometimes I’ll spawn a second AI session (or a different model) and ask <em>it</em> to critique or review code produced by the first. For example, I might have Claude write the code and then ask Gemini, “Can you review this function for any errors or improvements?” This can catch subtle issues. The key is to <em>not</em> skip the review just because an AI wrote the code. If anything, AI-written code needs <strong>extra scrutiny</strong>, because it can sometimes be superficially convincing while hiding flaws that a human might not immediately notice.</p><p>I also use <a href="https://github.com/chromeDevTools/chrome-devtools-mcp/">Chrome DevTools MCP</a>, built with my last team, for my <strong>debugging and quality loop</strong> to bridge the gap between static code analysis and live browser execution. It “gives your agent eyes”. It lets me grant my AI tools direct access to see what the browser can, inspect the DOM, get rich performance traces, console logs or network traces. This integration eliminates the friction of manual context switching, allowing for automated UI testing directly through the LLM. It means bugs can be diagnosed and fixed with high precision based on actual runtime data.</p><p>The dire consequences of skipping human oversight have been documented. One developer who leaned heavily on AI generation for a rush project <a href="https://albertofortin.com/writing/coding-with-ai#:~:text=No%20consistency%2C%20no%20overarching%20plan,the%20other%209%20were%20doing">described</a> the result as an inconsistent mess — duplicate logic, mismatched method names, no coherent architecture. He realized he’d been “building, building, building” without stepping back to really see what the AI had woven together. The fix was a painful refactor and a vow to never let things get that far out of hand again. I’ve taken that to heart. <strong>No matter how much AI I use, I remain the accountable engineer</strong>.</p><p>In practical terms, that means I only merge or ship code after I’ve understood it. If the AI generates something convoluted, I’ll ask it to add comments explaining it, or I’ll rewrite it in simpler terms. If something doesn’t feel right, I dig in — just as I would if a human colleague contributed code that raised red flags.</p><p>It’s all about mindset: <strong>the LLM is an assistant, not an autonomously reliable coder</strong>. I am the senior dev; the LLM is there to accelerate me, not replace my judgment. Maintaining this stance not only results in better code, it also protects your own growth as a developer. (I’ve heard some express concern that relying too much on AI might dull their skills — I think as long as you stay in the loop, actively reviewing and understanding everything, you’re still sharpening your instincts, just at a higher velocity.) In short: <strong>stay alert, test often, review always.</strong> It’s still your codebase at the end of the day.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xX4sJEPXb2yFqXIF.png" /></figure><h3>Commit often and use version control as a safety net. Never commit code you can’t explain.</h3><p><strong>Frequent commits are your save points — they let you undo AI missteps and understand changes.</strong></p><p>When working with an AI that can generate a lot of code quickly, it’s easy for things to veer off course. I mitigate this by adopting ultra-granular version control habits. I commit early and often, even more than I would in normal hand-coding. After each small task or each successful automated edit, I’ll make a git commit with a clear message. This way, if the AI’s next suggestion introduces a bug or a messy change, I have a recent checkpoint to revert to (or cherry-pick from) without losing hours of work. One practitioner likened it to treating commits as <strong>“save points in a game”</strong> — if an LLM session goes sideways, you can always roll back to the last stable commit. I’ve found that advice incredibly useful. It’s much less stressful to experiment with a bold AI refactor when you know you can undo it with a git reset if needed.</p><p>Proper version control also helps when collaborating with the AI. Since I can’t rely on the AI to remember everything it’s done (context window limitations, etc.), the git history becomes a valuable log. I often scan my recent commits to brief the AI (or myself) on what changed. In fact, LLMs themselves can leverage your commit history if you provide it — I’ve pasted git diffs or commit logs into the prompt so the AI knows what code is new or what the previous state was. Amusingly, LLMs are <em>really</em> good at parsing diffs and using tools like git bisect to find where a bug was introduced. They have infinite patience to traverse commit histories, which can augment your debugging. But this only works if you have a tidy commit history to begin with.</p><p>Another benefit: small commits with good messages essentially document the development process, which helps when doing code review (AI or human). If an AI agent made five changes in one go and something broke, having those changes in separate commits makes it easier to pinpoint which commit caused the issue. If everything is in one giant commit titled “AI changes”, good luck! So I discipline myself: <em>finish task, run tests, commit.</em> This also meshes well with the earlier tip about breaking work into small chunks — each chunk ends up as its own commit or PR.</p><p>Finally, don’t be afraid to <strong>use branches or worktrees</strong> to isolate AI experiments. One advanced workflow I’ve adopted (inspired by folks like Jesse Vincent) is to spin up a fresh git worktree for a new feature or sub-project. This lets me run multiple AI coding sessions in parallel on the same repo without them interfering, and I can later merge the changes. It’s a bit like having each AI task in its own sandbox branch. If one experiment fails, I throw away that worktree and nothing is lost in main. If it succeeds, I merge it in. This approach has been crucial when I’m, say, letting an AI implement Feature A while I (or another AI) work on Feature B simultaneously. Version control is what makes this coordination possible. In short: <strong>commit often, organize your work with branches, and embrace git</strong> as the control mechanism to keep AI-generated changes manageable and reversible.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dpTjWA8_zMQtmLe1.png" /></figure><h3>Customize the AI’s behavior with rules and examples</h3><p><strong>Steer your AI assistant by providing style guides, examples, and even “rules files” — a little upfront tuning yields much better outputs.</strong></p><p>One thing I learned is that you don’t have to accept the AI’s default style or approach — you can influence it heavily by giving it guidelines. For instance, I have a <strong>CLAUDE.md</strong> file that I update periodically, which contains process rules and preferences for Claude (Anthropic’s model) to follow (and similarly a GEMINI.md when using Gemini CLI). This includes things like “write code in our project’s style, follow our lint rules, don’t use certain functions, prefer functional style over OOP,” etc. When I start a session, I feed this file to Claude to align it with our conventions. It’s surprising how well this works to keep the model “on track” as Jesse Vincent <a href="https://blog.fsck.com/2025/10/05/how-im-using-coding-agents-in-september-2025/#:~:text=I%27m%20still%20primarily%20using%20Claude,Code">noted</a> — it reduces the tendency of the AI to go off-script or introduce patterns we don’t want.</p><p>Even without a fancy rules file, you can <strong>set the tone with custom instructions or system prompts</strong>. GitHub Copilot and Cursor both introduced features to let you configure the AI’s behavior <a href="https://benjamincongdon.me/blog/2025/02/02/How-I-Use-AI-Early-2025/#:~:text=stuck,my%20company%E2%80%99s%20%2F%20team%E2%80%99s%20codebase">globally</a> for your project. I’ve taken advantage of that by writing a short paragraph about our coding style, e.g. “Use 4 spaces indent, avoid arrow functions in React, prefer descriptive variable names, code should pass ESLint.” With those instructions in place, the AI’s suggestions adhere much more closely to what a human teammate might write. Ben Congdon <a href="https://benjamincongdon.me/blog/2025/02/02/How-I-Use-AI-Early-2025/#:~:text=roughly%20on%20par,get%20past%20a%20logical%20impasse">mentioned</a> how shocked he was that few people use <strong>Copilot’s custom instructions</strong>, given how effective they are — he could guide the AI to output code matching his team’s idioms by providing some examples and preferences upfront. I echo that: take the time to teach the AI your expectations.</p><p>Another powerful technique is providing <strong>in-line examples</strong> of the output format or approach you want. If I want the AI to write a function in a very specific way, I might first show it a similar function already in the codebase: “Here’s how we implemented X, use a similar approach for Y.” If I want a certain commenting style, I might write a comment myself and ask the AI to continue in that style. Essentially, <em>prime</em> the model with the pattern to follow. LLMs are great at mimicry — show them one or two examples and they’ll continue in that vein.</p><p>The community has also come up with creative “rulesets” to tame LLM behavior. You might have heard of the <a href="https://harper.blog/2025/04/17/an-llm-codegen-heros-journey/#:~:text=repository,it%20in%20a%20few%20steps">“Big Daddy” rule</a> or adding a “no hallucination/no deception” clause to prompts. These are basically tricks to remind the AI to be truthful and not overly fabricate code that doesn’t exist. For example, I sometimes prepend a prompt with: “If you are unsure about something or the codebase context is missing, ask for clarification rather than making up an answer.” This reduces hallucinations. Another rule I use is: “Always explain your reasoning briefly in comments when fixing a bug.” This way, when the AI generates a fix, it will also leave a comment like “// Fixed: Changed X to Y to prevent Z (as per spec).” That’s super useful for later review.</p><p>In summary, <strong>don’t treat the AI as a black box — tune it</strong>. By configuring system instructions, sharing project docs, or writing down explicit rules, you turn the AI into a more specialized developer on your team. It’s akin to onboarding a new hire: you’d give them the style guide and some starter tips, right? Do the same for your AI pair programmer. The return on investment is huge: you get outputs that need less tweaking and integrate more smoothly with your codebase.</p><h3>Embrace testing and automation as force multipliers</h3><p><strong>Use your CI/CD, linters, and code review bots — AI will work best in an environment that catches mistakes automatically.</strong></p><p>This is a corollary to staying in the loop and providing context: a well-oiled development pipeline enhances AI productivity. I ensure that any repository where I use heavy AI coding has a robust <strong>continuous integration setup</strong>. That means automated tests run on every commit or PR, code style checks (like ESLint, Prettier, etc.) are enforced, and ideally a staging deployment is available for any new branch. Why? Because I can let the AI trigger these and evaluate the results. For instance, if the AI opens a pull request via a tool like Jules or GitHub Copilot Agent, our CI will run tests and report failures. I can feed those failure logs back to the AI: “The integration tests failed with XYZ, let’s debug this.” It turns bug-fixing into a collaborative loop with quick feedback, which AIs handle quite well (they’ll suggest a fix, we run CI again, and iterate).</p><p>Automated code quality checks (linters, type checkers) also guide the AI. I actually include linter output in the prompt sometimes. If the AI writes code that doesn’t pass our linter, I’ll copy the linter errors into the chat and say “please address these issues.” The model then knows exactly what to do. It’s like having a strict teacher looking over the AI’s shoulder. In my experience, once the AI is aware of a tool’s output (like a failing test or a lint warning), it will try very hard to correct it — after all, it “wants” to produce the right answer. This ties back to providing context: give the AI the results of its actions in the environment (test failures, etc.) and it will learn from them.</p><p>AI coding agents themselves are increasingly incorporating automation hooks. Some agents will refuse to say a code task is “done” until all tests pass, which is exactly the diligence you want. Code review bots (AI or otherwise) act as another filter — I treat their feedback as additional prompts for improvement. For example, if CodeRabbit or another reviewer comments “This function is doing X which is not ideal” I will ask the AI, “Can you refactor based on this feedback?”</p><p>By combining AI with automation, you start to get a virtuous cycle. The AI writes code, the automated tools catch issues, the AI fixes them, and so forth, with you overseeing the high-level direction. It feels like having an extremely fast junior dev whose work is instantly checked by a tireless QA engineer. But remember, <em>you</em> set up that environment. If your project lacks tests or any automated checks, the AI’s work may slip through with subtle bugs or poor quality until much later.</p><p>So as we head into 2026, one of my goals is to bolster the quality gates around AI code contribution: more tests, more monitoring, perhaps even AI-on-AI code reviews. It might sound paradoxical (AIs reviewing AIs), but I’ve seen it catch things one model missed. Bottom line: <strong>an AI-friendly workflow is one with strong automation — use those tools to keep the AI honest</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Ce4ToIySL7vTx82U.png" /></figure><h3>Continuously learn and adapt (AI amplifies your skills)</h3><p><strong>Treat every AI coding session as a learning opportunity — the more you know, the more the AI can help you, creating a virtuous cycle.</strong></p><p>One of the most exciting aspects of using LLMs in development is how much <em>I</em> have learned in the process. Rather than replacing my need to know things, AIs have actually exposed me to new languages, frameworks, and techniques I might not have tried on my own.</p><p>This pattern holds generally: if you come to the table with solid software engineering fundamentals, the AI will <strong>amplify</strong> your productivity multifold. If you lack that foundation, the AI might just amplify confusion. Seasoned devs have observed that LLMs “reward existing best practices” — things like writing clear specs, having good tests, doing code reviews, etc., all become even more powerful when an AI is involved. In my experience, the AI lets me operate at a higher level of abstraction (focusing on design, interface, architecture) while it churns out the boilerplate, but I need to <em>have</em> those high-level skills first. As Simon Willison notes, almost everything that makes someone a <strong>senior engineer</strong> (designing systems, managing complexity, knowing what to automate vs hand-code) is what now yields the best outcomes with AI. So using AIs has actually pushed me to <strong>up my engineering game</strong> — I’m more rigorous about planning and more conscious of architecture, because I’m effectively “managing” a very fast but somewhat naïve coder (the AI).</p><p>For those worried that using AI might degrade their abilities: I’d argue the opposite, if done right. By reviewing AI code, I’ve been exposed to new idioms and solutions. By debugging AI mistakes, I’ve deepened my understanding of the language and problem domain. I often ask the AI to explain its code or the rationale behind a fix — kind of like constantly interviewing a candidate about their code — and I pick up insights from its answers. I also use AI as a research assistant: if I’m not sure about a library or approach, I’ll ask it to enumerate options or compare trade-offs. It’s like having an encyclopedic mentor on call. All of this has made me a more knowledgeable programmer.</p><p>The big picture is that <strong>AI tools amplify your expertise</strong>. Going into 2026, I’m not afraid of them “taking my job” — I’m excited that they free me from drudgery and allow me to spend more time on creative and complex aspects of software engineering. But I’m also aware that for those without a solid base, AI can lead to Dunning-Kruger on steroids (it may <em>seem</em> like you built something great, until it falls apart). So my advice: continue honing your craft, and use the AI to accelerate that process. Be intentional about periodically coding without AI too, to keep your raw skills sharp. In the end, the developer + AI duo is far more powerful than either alone, and the <em>developer</em> half of that duo has to hold up their end.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KwPNkwEgv-d6WvPH.png" /></figure><h3>Conclusion</h3><p>As we enter 2026, I’ve fully embraced AI in my development workflow — but in a considered, expert-driven way. My approach is essentially <strong>“AI-augmented software engineering”</strong> rather than AI-automated software engineering.</p><p>I’ve learned: <strong>the best results come when you apply classic software engineering discipline to your AI collaborations</strong>. It turns out all our hard-earned practices — design before coding, write tests, use version control, maintain standards — not only still apply, but are even more important when an AI is writing half your code.</p><p>I’m excited for what’s next. The tools keep improving and my workflow will surely evolve alongside them. Perhaps fully autonomous “AI dev interns” will tackle more grunt work while we focus on higher-level tasks. Perhaps new paradigms of debugging and code exploration will emerge. No matter what, I plan to stay <em>in the loop</em> — guiding the AIs, learning from them, and amplifying my productivity responsibly.</p><p>The bottom line for me: <strong>AI coding assistants are incredible force multipliers, but the human engineer remains the director of the show.</strong></p><p>With that…happy building in 2026! 🚀</p><p><em>I’m excited to share I’ve released a new </em><a href="https://beyond.addy.ie/"><em>AI-assisted engineering book</em></a><em> with O’Reilly. There are a number of free tips on the book site in case interested.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=52fe1681325e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Effective Software Engineers: my free new Google video series]]></title>
            <link>https://medium.com/@addyosmani/effective-software-engineers-my-free-new-google-video-series-361317933cae?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/361317933cae</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[developer]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Sun, 14 Dec 2025 22:22:16 GMT</pubDate>
            <atom:updated>2025-12-14T22:22:16.512Z</atom:updated>
            <content:encoded><![CDATA[<p>After ~14 years at Google leading engineering teams, I’ve had many conversations with engineers at every level about what separates good from great. The answer isn’t what most people expect. It’s <strong>effectiveness — the ability to consistently focus your efforts on work that creates real user and business impact, not just activity.</strong></p><p>That’s why, together with <a href="https://medium.com/u/991272e72e68">Google Developers</a>, we’re releasing a <a href="https://www.youtube.com/watch?v=lqcKquUl3Wk&amp;t=7s">free new video series</a> exploring the core principles that drive engineering effectiveness, from your first commit to staff-level leadership. The first few episodes just dropped in case you’d like to check them out.</p><h3><strong>What is engineering effectiveness?</strong></h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FlqcKquUl3Wk%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlqcKquUl3Wk&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FlqcKquUl3Wk%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/acaca3e36810841fba567f59917896a2/href">https://medium.com/media/acaca3e36810841fba567f59917896a2/href</a></iframe><p>We’ll start by unpacking a critical distinction that changes how you think about your work: the difference between being efficient and being effective. You can be incredibly efficient at doing the wrong things, but that won’t move the needle. I’ll share insights from Google’s research on what actually drives impact, including findings from Project Aristotle on psychological safety and Project Oxygen on effective management.</p><p><strong>Most importantly, you’ll learn how to shift your mindset from measuring outputs (lines of code written) to focusing on outcomes (problems solved for users).</strong></p><h3><strong>The Principles of effective software engineering</strong></h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FP0flg_P8TK0%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DP0flg_P8TK0&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FP0flg_P8TK0%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/ebb445161775fc3349683bbf6a85a003/href">https://medium.com/media/ebb445161775fc3349683bbf6a85a003/href</a></iframe><p>Whether you’re just starting out or looking to strengthen your fundamentals, this episode covers the bedrock practices that compound over your entire career. We’ll explore what makes code truly maintainable, why testing is your highest-leverage activity (not just a chore), and h<strong>ow to build a quality mindset that makes you invaluable to any team</strong>. Think of this as the foundation that enables everything else.</p><h3><strong>How to become a T-Shaped software engineer</strong></h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FIMHneaMO-dg%3Fstart%3D1%26feature%3Doembed%26start%3D1&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIMHneaMO-dg&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FIMHneaMO-dg%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/ca6e7f1d9a0aef7e3c32bd01dd67e04b/href">https://medium.com/media/ca6e7f1d9a0aef7e3c32bd01dd67e04b/href</a></iframe><p>As you grow into senior and staff roles, technical excellence alone isn’t enough. <strong>This episode introduces the T-shaped model: developing deep expertise in one area while building broad knowledge across your stack</strong>. But it goes beyond technical skills. You’ll discover how to amplify your impact through cross-functional collaboration, learn to influence without authority, and master the art of managing up to ensure your work aligns with what truly matters.</p><h3>Who this series is for</h3><p>Whether you’re a new grad trying to make sense of your first sprint, a mid-level engineer wondering how to level up, or a senior engineer looking to refine your approach to leadership, there’s something here for you.</p><p>The series covers practical frameworks that worked for me while shipping products that serve billions of users and mentoring many engineers.</p><p>Watch the series on YouTube, and let us know what resonates with you. The shift from busy to impactful doesn’t happen overnight, but understanding these principles is the first step :)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rfcJ1SlRLIeKr3IxE8zpKw.jpeg" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=361317933cae" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Vibe coding is not the same as AI-Assisted engineering.]]></title>
            <link>https://medium.com/@addyosmani/vibe-coding-is-not-the-same-as-ai-assisted-engineering-3f81088d5b98?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/3f81088d5b98</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[cursor]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Sun, 30 Nov 2025 19:15:24 GMT</pubDate>
            <atom:updated>2025-11-30T19:15:24.511Z</atom:updated>
            <content:encoded><![CDATA[<p><em>You can find more of my writing on my </em><a href="https://addyo.substack.com"><em>Substack</em></a><em> and </em><a href="https://beyond.addy.ie"><em>Beyond Vibe Coding</em></a><em>.</em></p><p><strong>Vibe coding is not the same as AI-Assisted engineering.</strong> A recent <a href="https://www.reddit.com/r/vibecoding/comments/1myakhd/how_we_vibe_code_at_a_faang/#:~:text=Software%20development.,Go%20to%20comments%201%20Share">Reddit post</a> described how a FAANG team uses AI and it sparked an important conversation about semantics: “vibe coding” and professional “AI-assisted engineering”.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8Yj2IhsQL4QQTLnd.jpeg" /></figure><p>While the post was framed as an example of the former, the process it detailed — complete with technical design documents, stringent code reviews, and test-driven development — is a clear example of the latter imo. This distinction is critical because conflating the two risks both devaluing the discipline of engineering and giving newcomers a dangerously incomplete picture of what it takes to build robust, production-ready software.</p><p><strong>Vibe coding is great for momentum, but without structure it collapses under production demands.</strong></p><p>As a reminder: “vibe coding” is about fully giving in to the creative flow with an AI (high-level prompting), essentially forgetting the code exists. It involves accepting AI suggestions without deep review and focusing on rapid, iterative experimentation, making it ideal for prototypes, MVPs, learning, and what Karpathy calls “throwaway weekend projects.” This approach is a powerful way for developers to build intuition and for beginners to flatten the steep learning curve of programming. It prioritizes speed and exploration over the correctness and maintainability required for professional applications.</p><p>There is a spectrum between vibe coding and doing it with a little more planning, spec-driven development, including enough context etc and what is AI-assisted engineering across the software development lifecycle.</p><p>In contrast to the post, the process described in the Reddit post is a methodical integration of AI into a mature software development lifecycle. This is “AI-assisted engineering,” where AI acts as a powerful collaborator, not a replacement for engineering principles. In this model, developers use AI as a “force multiplier” to handle tasks like generating boilerplate code or writing initial test cases, but always within a structured framework.</p><p><strong>Crucially, the big difference here is the human engineer remains firmly in control, responsible for the architecture, reviewing and understanding every line of AI-generated code, and ensuring the final product is secure, scalable, and maintainable.</strong> The 30% increase in development speed mentioned in the post is a result of augmenting a solid process, not abandoning it.</p><p>For engineers, labeling disciplined, AI-augmented workflows as “vibe coding” misrepresents the skill and rigor involved. For those new to the field, it creates the false and risky impression that one can simply prompt their way to a viable product without understanding the underlying code or engineering fundamentals. If you’re looking to do this right, start with a solid design, subject everything to rigorous human review, and treat AI as an incredibly powerful tool in your engineering toolkit — not as a magic wand that replaces the craft itself.</p><h3>Vibe Coders, Rodeo Cowboys, and Prisoners</h3><p>The community is split: optimists see a revolution, skeptics see old cowboy coding in new clothes. Optimists call vibe coding the next abstraction layer — like moving from assembly to Python. Outsiders will push it forward until it works.</p><p>Realists use it for spikes, but enforce discipline after. <em>Use AI like a junior dev: helpful, but never unsupervised. </em>Skeptics dismiss it as marketing spin — <em>If I can tell it’s vibe-coded, it’s bad. Good software is good software.</em> The consensus middle-ground is pragmatic: vibe coding is a sandbox for creativity, but scaling demands engineering.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3hc4kj2bSDGOK_jf.jpeg" /></figure><p><em>A fun Venn diagram by </em><a href="https://forrestbrazeal.com/"><em>Forrest Brazeal</em></a><em> contrasts three engineering personas in the age of AI assistance — “Vibe Coders,” “Rodeo Cowboys,” and “Prisoners” — through the metaphor of</em> <em>rope</em> <em>(freedom vs. constraint). Each archetype highlights extreme approaches that are</em> <em>unlikely to deliver production‑grade software.</em></p><h3>Rope, Risk, and Developer Personas in AI Coding</h3><p>In the context of software development, “rope” represents the level of freedom and risk a developer is allowed — or allows themselves — when building.</p><p><strong>“Rodeo Cowboys” thrive with <em>too much</em> rope</strong>, embracing a wild-west style of coding with high risk tolerance and minimal oversight. They’ll happily lasso together new features or fixes on the fly, sometimes <em>literally</em> riding on adrenaline. <strong>“Prisoners” operate with <em>too little</em> rope</strong>, bound by rigid constraints, heavy governance, or fear of mistakes — they move slowly and cautiously, if at all. And in between lies the modern <em>AI-powered</em> coder: <strong>“Vibe Coders” are given <em>just enough</em> rope to hang themselves</strong>.</p><p>They rapidly generate code by prompting AI with natural-language “vibes” and trust the output, often without fully understanding or testing it.</p><p>Each persona has a distinct relationship with AI assistance and risk:</p><ul><li><strong>Vibe Coders</strong> — These developers collaborate with large language models (LLMs) in a <em>free-flowing, conversational</em> manner, describing what they want and letting the AI fill in the implementation. It’s an exhilarating level of freedom — <em>“just tell the AI to add a login page or fix this bug”</em>. The <strong>upside</strong> is speed and creativity; vibe coders act more as <em>orchestrators</em> than manual coders, focusing on ideas over syntax. But the <strong>downside</strong> is a lack of control. They often run with no safety harness: minimal code review, sparse tests, and blind trust in AI outputs. In other words, a <em>lot of rope with little guidance</em>, which can result in codebases that are brittle and opaque. One engineer noted that vibe coding without review is like <a href="https://www.reddit.com/r/programming/comments/1l4x5tu/the_illusion_of_vibe_coding_there_are_no/"><em>“an electrician just threw a bunch of cables through your walls and hoped it all worked out, instead of running them with intention”</em></a> — things might function initially, but hidden flaws lurk behind the walls.</li><li><strong>Rodeo Cowboys</strong> — The classic “cowboy coder” isn’t new to software engineering, and in the AI era this persona still exists (sometimes <em>augmented</em> by AI, sometimes not). Rodeo cowboys are those developers who push code to production with daring speed and little process — they’ll prototype in production, hotfix live systems at 2 AM, and generally <strong>embrace risk in exchange for velocity</strong>. They have <strong>high risk tolerance</strong> (shared with vibe coders) but may rely more on their own intuition and experience than on AI. If vibe coders are guided by AI “voices,” rodeo cowboys follow their gut. They do have <em>some</em> rope constraints (even rodeos occur in fenced arenas), but often just enough rope to nearly hang themselves. The overlap of these styles is obvious: a vibe coder can <em>become</em> a rodeo cowboy when they start shipping AI-generated code directly to production in a blaze of glory. The results can be spectacular… or disastrous.</li><li><strong>Prisoners</strong> — On the opposite extreme, we have engineers who are so constrained by process, bureaucracy, or self-imposed caution that they can barely move. These “prisoners” might work in heavily regulated industries or legacy systems where every line of code is a battle. They have <strong>almost no rope</strong> — tight guardrails, mandatory approvals, perhaps skepticism or outright bans on AI assistance. While this mindset ensures safety and predictability, it also stifles innovation. Prisoner-type developers may watch the AI revolution from the sidelines, unable to partake due to organizational rules or fear of the unknown. They won’t hang themselves with rope because they’re never given any slack, but they also might not deliver anything new and exciting. Interestingly, prisoners and vibe coders share one trait: <strong>being “ordered around by disembodied voices.”</strong> In the prisoner’s case, the voices are process checklists, ticketing systems, or bureaucratic policies dictating every move — whereas for vibe coders it’s the AI’s suggestions. Neither is truly in control.</li></ul><p>In reality, engineers aren’t binary labels — a single person might embody elements of all three personas depending on the project and pressures. The Venn diagram’s punchline is that all three extremes fail in the long run: too much freedom <em>or</em> too many constraints both hinder sustainable engineering.</p><p><strong>The key is finding balance — giving developers enough rope to innovate, but not so much that they (or the codebase) end up strangled by bugs and technical debt.</strong> The rest of this report explores why unchecked “vibe coding” has drawn sharp criticism from industry leaders and online communities, and how teams can harness AI-assisted development more responsibly.</p><p><strong>Bottom line from the community:</strong> vibe coding accelerates exploration but introduces hidden liabilities that blow up in production. Recent discussions highlight recurring problems:</p><ul><li><a href="https://www.vktr.com/ai-technology/vibe-coding-explained-use-cases-risks-and-developer-guidance/">Security flaws</a>: API keys left in code, no input sanitization, or naive auth logic</li><li><a href="https://www.reddit.com/r/programming/comments/1l4x5tu/the_illusion_of_vibe_coding_there_are_no/">Fragile debugging</a>: non-engineers hit walls when even minor changes cause cascading failures.</li></ul><h3>Will you vibe code your way to production?</h3><p>Across the software industry, seasoned engineering leaders are issuing a clear warning: <strong>AI-assisted “vibe coding” may rapidly create more problems than it solves in a production codebase.</strong> <a href="https://www.vktr.com/ai-technology/vibe-coding-explained-use-cases-risks-and-developer-guidance/">Canva’s CTO Brendan Humphreys</a> captured this sentiment bluntly:</p><blockquote><strong><em>“No, you won’t be vibe coding your way to production — not if you prioritize quality, safety, security and long-term maintainability at scale.”</em></strong></blockquote><p>These tools require <em>careful supervision by skilled engineers</em>, especially for mission-critical code. In other words, AI can accelerate development, but it cannot replace the hard disciplines of software engineering. When those disciplines are skipped, the result is often a fragile system and a mountain of hidden issues, as <a href="https://www.reddit.com/r/vibecoding/comments/1mge229/my_vibe_coding_journey/">visualized</a> by /r/vibecoding:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*h9Tsmcav8VG3vzgl.jpeg" /></figure><p>Recent findings back up these warnings. In an August 2025 survey by <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding">Final Round AI</a>, <strong>18 CTOs were asked about vibe coding and 16 reported experiencing production disasters directly caused by AI-generated code</strong>. These are tech leaders with no incentive to hype a trend — their perspective comes from hard lessons in the field. As one summary <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding">put it</a>:</p><blockquote><em>“AI promised to make us all 10x developers, but instead it’s making juniors into prompt engineers and seniors into code janitors cleaning up AI’s mess.”</em></blockquote><p>The ability to ship features faster means little when those features are riddled with flaws that wake someone up at 2 AM.</p><p><strong>What kinds of failures are we talking about?</strong> The CTOs gave examples spanning performance meltdowns, security breaches, and maintainability nightmares:</p><ul><li>A <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding"><strong>performance disaster</strong></a> was recounted by a CTO who watched an AI-generated database query <em>work perfectly in testing</em> but then bring their system to its knees in production. The query was syntactically correct (no obvious errors), so the developer assumed it was fine. But it was <em>woefully inefficient</em> at scale, something an experienced engineer or a proper code review could have caught. <em>“It worked for a small dataset, but as soon as real-world traffic hit, the system slowed to a crawl,”</em> the CTO said. The team wasted a week debugging why the app was hanging — a week they would not have lost had the code been thoughtfully designed. This highlights a key danger: <strong>AI doesn’t understand your system’s architecture or non-functional requirements</strong> unless you explicitly guide it. It can produce code that <em>looks</em> good and passes basic tests, yet falls apart under real workloads. As another leader put it, vibe coding creates an illusion of success until <em>“the system begins to wobble under workloads”</em> — then it <strong>catastrophically fails without warning</strong>.</li><li>A <strong>security lapse</strong> was described by an architect who caught a devastating bug in an AI-written authentication module. A junior developer had “vibed” their way through building a user permissions system by copy-pasting AI suggestions and Stack Overflow snippets. It passed the initial tests and even QA. But two weeks after launch, they discovered a critical flaw: <strong>users with deactivated accounts still had access to certain admin tools</strong>. The AI had inverted a truthy check (e.g. using a negation incorrectly), a subtle bug that slipped through. Because no one deeply understood that autogen code, the issue went unnoticed. <em>“It seemed to work at the time,”</em> the developer had said. This is a classic AI-generated mistake — logically inverted logic that a human might catch if they wrote it, but the AI’s code was treated as a black box. The result was a serious security breach. A senior engineer spent two days untangling that one-line bug in a sea of AI code. The architect dubbed this <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding"><strong>“trust debt”</strong></a> — <em>“it puts pressure on your senior engineers to be permanent code detectives, reverse-engineering vibe-driven logic just to ship a stable update.”</em> <strong>In other words, every time you trust AI output without verification, you incur a debt that must be paid by someone combing through that code later to actually understand and fix it.</strong></li><li>A <strong>maintainability and complexity nightmare</strong> came from a story of an AI-generated feature that technically worked fine… until requirements changed. One team allowed a developer to vibe-code an entire user authentication flow with AI, stitching together random npm packages and Firebase rules in minutes. Initially, <em>“on the surface, things shipped — clients were happy, everyone’s high-fiving,”</em> said one engineering manager. But when the team later needed to extend the auth system for new roles and region-specific privacy rules, <em>“it collapsed. No one could trace what was connected to what. Middleware was scattered across six files. There was</em> <em>no mental model, just vibes.”</em> In the end, they had to <strong>rewrite the whole thing from scratch</strong>, because debugging the AI’s spaghetti code was <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding"><em>“like archaeology.”</em></a> This highlights how <strong>lack of structure and consistency in AI outputs can lead to unmaintainable code.</strong></li><li>A <strong>false sense of security</strong> is another insidious danger. AI-generated code often <em>appears perfectly neat and even</em> idiomatic*. It might pass unit tests you wrote. So developers let their guard down. One CTO observed that vibe coding’s most dangerous characteristic is code that <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding">“appears to work perfectly until it <strong>catastrophically fails</strong>.”</a> It lulls teams into production with a smile, then bites hard. Even code review can fail here: reviewing a 1000-line AI-written PR is not much easier than writing it from scratch, especially if the reviewer assumes the code is mostly correct. And if an AI is used to assist code review as well (yes, that’s a thing), then we get the blind leading the blind — <a href="https://shiftmag.dev/the-illusion-of-vibe-coding-5297/">“trusting machines to verify machines”</a>, as one commentary put it. Maintainability suffers because no one truly owns the code’s logic.</li></ul><p><strong>The consensus from leaders is that vibe coding puts critical software qualities at risk: security, clarity, maintainability, and team knowledge.</strong></p><p>In summary, industry leaders aren’t luddites resisting a new technology — they’re the ones responsible for keeping systems running reliably. Their message is a <strong>critical but constructive</strong> one: <em>use AI to assist, not to abdicate.</em> Code still needs human judgment, especially if it’s destined for production. As one veteran put it, <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding"><strong>“AI tools are copilots, not autopilots.”</strong></a> They can help fly the plane, but a human pilot must chart the course and be ready to grab the controls when turbulence hits.</p><h3>“This isn’t engineering, it’s hoping.”</h3><p>It’s not just CTOs and thought leaders sounding the alarm — the rank-and-file developer community (the ones in the trenches with AI tools daily) have been vigorously debating vibe coding throughout 2025. On <a href="https://www.reddit.com/r/programming/comments/1l4x5tu/the_illusion_of_vibe_coding_there_are_no/">Reddit</a> and <a href="https://news.ycombinator.com/item?id=44959069">Hacker News</a>, threads about vibe coding have garnered hundreds of upvotes, with seasoned developers sharing war stories and sharp critiques, as well as a few counterexamples and success stories. The overall mood: <strong>high skepticism of using un-reviewed AI code in serious projects, mixed with some optimism for limited use cases.</strong></p><p>On the critical side, developers recount how vibe coding has negatively impacted their workflows and team dynamics. A top comment on one Reddit thread <a href="https://www.reddit.com/r/programming/comments/1l4x5tu/comment/mwd6n9j/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button">lamented</a>:</p><blockquote><em>“I just wish people would stop pinging me on PRs they obviously haven’t even read themselves, expecting me to review 1000 lines of completely new vibe-coded feature that isn’t even passing CI.”</em></blockquote><p>The frustration here is palpable — <em>code review</em> becomes a farce if the author themselves doesn’t understand the AI-generated code or bother to run tests. It shifts the burden onto unwitting teammates. Another commenter <a href="https://www.reddit.com/r/programming/comments/1l4x5tu/comment/mwdhsz8/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button">responded</a> that this behavior <em>“feels so far below the minimum bar of professionalism”</em>, likening it to a tradesperson doing a shoddy job that others have to fix. <strong>Peer review and team trust break down</strong> when one developer dumps AI output on others without due diligence.</p><p>The cultural backlash is evident — nobody wants to work with a so-called engineer whose contribution is copy-pasting from ChatGPT and shrugging when things break.</p><p>The phrase <a href="https://shiftmag.dev/the-illusion-of-vibe-coding-5297/"><em>“this isn’t engineering, it’s hoping”</em></a> has been used in these discussions (a paraphrase of a line from the ShiftMag article). It captures the sentiment that vibe coding without proper review/testing is akin to hoping the software works by magic. Many developers point out that <strong>coding is supposed to be an engineering discipline</strong>, not wish fulfillment.</p><p>However, amid the criticism, there are also <strong>counterpoints and nuanced views</strong> emerging from the community. Not everyone is writing off vibe coding entirely; some are experimenting and finding <em>niche scenarios where it excels</em>. A highly upvoted <a href="https://news.ycombinator.com/item?id=44959069">Hacker News comment</a> framed it through the classic <a href="https://news.ycombinator.com/item?id=44959069">Innovator’s Dilemma</a>: today’s experts dismiss vibe coding as a toy, but tomorrow it could evolve and render old methods obsolete. While many responses disagreed with the inevitability of that outcome, the discussion opened up a more <em>optimistic</em> perspective: maybe we’re just in the early clunky phase of AI coding, and improvements in AI or methodologies will address current flaws.</p><p>A practical example was <a href="https://news.ycombinator.com/item?id=44960238">given</a> by one HN user who successfully <strong>vibe-coded a bespoke internal tool</strong> in record time.</p><blockquote><em>“Last week I converted a bunch of Docker Compose configs to run on Terraform (Opentofu) — took me maybe an hour or two with Claude, while watching TV. Would’ve been a week easy if I did it the artisanal way of reading docs and Stack Overflow.”</em></blockquote><p>This developer wasn’t building a customer-facing product, but rather automating a tedious infrastructure task. For that use-case, vibe coding was a huge win: it saved time and the code was “good enough” for an internal tool that he controlled. Many others chimed in to agree that <strong>for small-scale or one-off scripts and glue code, vibe coding can be a massive productivity booster</strong>. It’s the classic 80/20 trade-off — if you need something quick and you’re the only stakeholder, the AI can crank out a solution in minutes instead of days.</p><p>Crucially, the developer in this story <em>knew the limits</em>: he treated the AI as an assistant to speed up his own work (even multitasking entertainment while coding), and presumably he validated the output. This aligns with a refrain seen in several discussions: <em>“Good time saver,</em> <em>if you know what you’re doing.”</em> The implication is that an experienced dev can use vibe coding like a power tool — accelerating the grunt work — but they still architect, guide, and double-check the result. In inexperienced hands, the same power tool can wreak havoc.</p><p>There’s also an interesting analogy drawn in these debates: <a href="https://news.ycombinator.com/item?id=44959069"><strong>“Coding with agentic LLMs is just project management.”</strong></a> Instead of writing code, your job becomes breaking down tasks for the AI, verifying each chunk, and integrating the results — essentially acting as a project manager for a very junior (but very fast) developer. Some developers say this <em>feels</em> easier or lazier — hence “vibe” — while others find it still requires solid development chops, just applied differently. The ones who succeed treat it systematically: break problems into small, verifiable prompts (tasks that fit in context windows), run tests at each step, and iterate. The ones who fail just throw a big vague prompt at GPT and paste whatever comes out.</p><p><strong>Bottom line from the community:</strong> <em>Vibe coding is not a silver bullet.</em> Experienced devs mostly view it as a fun or useful technique for prototyping and automating trivial tasks, <strong>not</strong> a replacement for disciplined development on <a href="https://news.ycombinator.com/item?id=44959069">complex systems</a>. The hype that “LLMs will write all our software” is being met with healthy skepticism on the ground. At the same time, there’s recognition that AI coding assistants are here to stay and <em>can</em> provide huge productivity boosts when <a href="https://www.vktr.com/ai-technology/vibe-coding-explained-use-cases-risks-and-developer-guidance/">used judiciously</a>. The focus is shifting toward <strong>how to integrate AI into workflows without losing the rigor of software engineering</strong> — which we’ll explore next.</p><h3>Where can Vibe Coding help?</h3><p>If pure vibe coding is risky for production, is it <em>good for anything</em>? The answer from both industry leaders and practitioners is <em>yes</em>: <strong>vibe coding shines in certain scenarios — especially in rapid prototyping, exploratory projects, and as a creative aid — but one must know when to stop vibing and start engineering</strong>.</p><p>Several CTOs in the <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding">FinalRound survey</a> acknowledged that they don’t <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding">“condemn vibe coding entirely”</a>. Instead, they <strong>compartmentalize its use</strong> to get the best of both worlds. <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding">Matt Cumming</a>, founder at LittleHelp, shared that he can <em>“create and deploy a functional micro-SaaS web app in a day with no issues, which is insane.”</em> Over a weekend, he took an idea from thought to a live product by leveraging AI for the heavy lifting. This kind of speed is unprecedented — essentially compressing what might be a 2–3 week MVP build into 48 hours. <strong>For hackathons, demos, internal tools, and validating product-market fit quickly, vibe coding can be a game-changer.</strong> It allows small teams (or even solo devs) to punch far above their weight in terms of feature output.</p><p>However — and this is a big caveat — every leader who touted such successes added <em>major warnings and limits</em>. Matt Cumming himself had learned the hard way that unleashing AI without guardrails can backfire. He described how an early project of his was <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding"><em>“completely destroyed by AI in a few minutes”</em></a> after a month of work, due to some AI-generated corruption or error. Chastened by that experience, he established a disciplined approach:</p><blockquote><em>“We start any AI-assisted coding by collaborating with the AI to write the functional spec and steps required in a project document. The other crucial thing I learned is to get the agent to do a security check *on any new functionality you add.”</em></blockquote><p>In other words, <strong>use the AI to help plan and review, not just to spit out code</strong>. By having the AI outline the design first, he ensures he’s thought through the architecture. By having it perform security analysis on the output, he adds an automated sanity check for vulnerabilities. His team also confines vibe coding to “throwaway projects and prototypes, not production systems that need to scale”. The vibe-coded app serves as a proof of concept, which they might later <em>rewrite or harden</em> for real-world use.</p><p>Another leader, <a href="https://www.finalroundai.com/blog/what-ctos-think-about-vibe-coding">Brett Farmiloe</a> (CEO at Featured), echoed this:</p><blockquote><em>“Vibe coding is great when you’re starting from scratch… we built a site using v0 (an AI tool) and deployed quickly. But with our established production codebase, we only use vibe-coded components as a starting point — then technical team members take over to finish.”</em></blockquote><p>Both he and Cumming treat AI-generated code as <strong>scaffolding</strong>. It’s fantastic to erect a structure rapidly, see if it holds the shape you want, but you wouldn’t leave the rickety scaffolding in place for the final building. You replace or reinforce it with solid materials. <strong>In software terms: the AI prototype must be refactored, tested, and owned by human engineers before it becomes the permanent solution.</strong></p><p>Another approved use case is <a href="https://www.vktr.com/ai-technology/vibe-coding-explained-use-cases-risks-and-developer-guidance/"><strong>legacy code refactoring</strong></a>. As noted in one analysis, some companies let AI rewrite portions of old code into newer frameworks or languages as a “head start”. Since that code is going to be reviewed and tested thoroughly anyway, using AI to do the brute-force translation or rote work can save time. Similarly, AI can help squash known bug patterns: e.g. <em>“solve these 5 specific defects in our code”</em> — a targeted application rather than carte blanche generation. In these cases, the <strong>scope is limited and the outputs are verified</strong>, aligning more with <strong>AI-assisted programming</strong> than blind vibe coding.</p><p>We can summarize where vibe coding <em>adds the most value</em> in 2025:</p><ul><li><strong>Rapid Prototyping &amp; hackathons:</strong> Need to demo an idea by tomorrow? AI code generation can materialize a working prototype incredibly fast. It’s okay if the code is messy, as long as it showcases the concept. <strong>Speed and iteration matter more than robustness</strong> at this stage. Vibe coding lets you try three different approaches in a day — something traditional coding would never allow.</li><li><strong>One-off scripts and internal tools:</strong> If the code doesn’t need long-term maintenance and is only used by the author, the risk is relatively low. Writing a quick data analysis script, converting file formats, automating server configs — those kinds of tasks can often be safely vibe-coded because if it breaks, the person who wrote it (with AI) will fix it, and there aren’t many external consequences. Basically, <strong>personal projects or automation</strong> are fertile ground for vibe coding, saving engineers from drudge work.</li><li><strong>Greenfield development (with caution):</strong> Starting a new project from scratch is easier to vibe code than integrating AI into a sprawling legacy system. When nothing exists yet, there are fewer constraints and no established style to adhere to. A team might vibe code the first version of a new microservice or frontend app to hit an aggressive deadline, then polish it later.</li></ul><p>In all these scenarios, though, it’s assumed that <strong>experienced developers are in the loop</strong>. The people achieving success with vibe coding still apply their judgment to prompt the AI effectively and to verify the outputs. They treat it as <strong>pair programming with a tireless but error-prone junior dev</strong>. Contrast this with novices who might think the AI is a magic shortcut to skip learning — that approach often ends in frustration. Vibe coding, if overused by beginners, can short-circuit the learning process and produce grads who have impressive projects on their resume but lack fundamental skills — another long-term risk noted by educators. Ultimately, when paired with human experience, it can meaningfully reduce time on menial work, but you likely can’t <a href="https://www.reddit.com/r/vibecoding/comments/1mu6t8z/whats_the_point_of_vibe_coding_if_i_still_have_to/">skip</a> human review and refinement entirely:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PJlXawULwleS4gLk.png" /></figure><p><strong>The key insight is that vibe coding can be extremely valuable as an ideation and acceleration tool, but it should almost never be the <em>last step</em> for anything that lives on.</strong> Use it to get from zero to demo, or to churn out boilerplate, or explore multiple options. Then comes the crucial phase: <em>the hand-off from vibes to rigorous engineering</em>. How to do that effectively is our next focus.</p><h3>Spec-Driven: An antidote to prompt-chaos?</h3><p>One promising development in response to the pitfalls of vibe coding is the rise of <strong>spec-driven and </strong><a href="https://medium.com/@takafumi.endo/software-3-0-blueprint-from-vibe-coding-to-verified-intelligence-swarms-23b4537f12fa"><strong>“agentic” AI coding approaches</strong></a>. These methods aim to retain the productivity benefits of AI generation while introducing more structure, planning, and verification — essentially adding <em>rails</em> to the free-form vibe coding process.</p><p><strong>Spec-driven AI development</strong> means starting with a clear specification or design, often created in collaboration with the AI, <em>before</em> any code is written. Instead of immediately prompting “Hey AI, build me a feature,” an engineer might prompt the AI to “Help me outline the requirements and modules for this feature.” By having an explicit spec (be it a written paragraph, a formal design doc, or a list of steps and functions), the developer ensures both they and the AI have a shared understanding of the goal. It’s akin to writing pseudo-code or user stories first. This addresses one major issue of vibe coding: lack of direction. Writing a functional spec with the AI can keep a team on track and prevent the AI from wandering off into irrelevant complexity.</p><p>Practically, spec-driven workflow can involve <strong>prompting the AI to generate high-level plans, interface definitions, and even test cases up front</strong>, and iterating on those until the human is satisfied that the plan makes sense. Only then do they ask the AI to implement the pieces of that plan. This is similar to how good engineers work with juniors — you wouldn’t let a junior developer code an entire subsystem solo on day one; you’d first agree on a design together. With AI, we’re learning to do the same: treat it like a junior that needs a blueprint. Early evidence suggests this yields better results than ad-hoc prompting.</p><p><a href="https://medium.com/@takafumi.endo/software-3-0-blueprint-from-vibe-coding-to-verified-intelligence-swarms-23b4537f12fa"><strong>Agentic AI approaches</strong></a> take this further by enabling AI to not only follow spec but also <strong>perform some self-directed actions like running code, testing, and refining</strong>. The term “agentic” here refers to AI agents that can take higher-level goals and then act in an autonomous, iterative way to achieve them (within bounds). For example, some tools allow the AI to do things like execute the code it wrote, observe the results, and fix errors — all without the user explicitly asking at each step.</p><p>Spec-driven and agentic approaches contrast with naive vibe coding in a few key ways:</p><ul><li><strong>Upfront intent vs. after-the-fact fixes:</strong> Instead of writing code and then trying to retrofit understanding (or just hoping it works), the spec-first approach encodes intent clearly from the start. The AI’s output is judged against a known target. This reduces the “surprise” factor of AI code that technically does something you asked but not in the way you wanted (a common complaint when prompts are not specific enough).</li><li><strong>Small iterations vs. big bang:</strong> Agentic workflows encourage small, testable increments. Rather than asking for a thousand-line program in one go, you ask for one function, see it pass tests, then proceed. Essentially, it mimics <strong>test-driven development</strong> but with AI as the one writing the implementation from your test descriptions. If vibe coding is like typing a novel in one prompt, spec-driven is like pair-writing it chapter by chapter with continuous editorial review.</li><li><strong>AI in the loop vs. human alone:</strong> Interestingly, agentic approaches can also take <em>more</em> burden off humans in some respects by letting the AI handle tedious verification steps. For instance, if every AI-written PR must come with an explanation of <em>why</em> the changes were made, an AI agent can be tasked with generating an initial draft of that explanation, which the developer then edits for accuracy. This ensures no code is merged without context. In effect, <strong>these approaches try to weave AI into the fabric of software development best practices — not replace them</strong>. Instead of ignoring testing and review (as vibe coding often does), they automate parts of testing and review.</li></ul><p>In practice, teams exploring these approaches use tactics like:</p><ul><li><strong>Requiring design docs</strong> for any major AI-generated component (even if it’s one page, written with AI assistance). This ensures thought was given to how the component fits the system.</li><li><strong>Using AI to generate unit tests</strong> or property-based tests for its own code, catching obvious errors immediately.</li><li><strong>Locking down dependencies and focusing on security</strong>: For example, instructing the AI to <em>only</em> use approved libraries and run a security scan. As noted, one team actually uses vibe coding in a <a href="https://www.vktr.com/ai-technology/vibe-coding-explained-use-cases-risks-and-developer-guidance/"><em>controlled way to intentionally generate insecure code</em></a><em>, so they can study it and improve their security scanners</em> — turning the AI into a pen-testing tool rather than a production coder.</li><li><strong>Preferring integrated AI tools (in-IDE like Cursor or VS Code Copilot)</strong> over copy-paste from ChatGPT. Integration means the AI suggestions are applied in a context where the developer can see the entire diff and run the code immediately, reducing the chance of inadvertently introducing something you don’t notice.</li><li><strong>Keeping humans in the decision loop</strong>: e.g. an AI agent might propose a code change, but it can’t merge it — a human must approve. This is analogous to how continuous integration (CI) systems run tests and linters, but ultimately a dev checks the PR. The AI is a CI assistant here, not an autorobot deploying to prod on its own.</li></ul><p>All these measures are attempts to <strong>mitigate the “vibes” with actual engineering rigor</strong>. They acknowledge that large language models are incredibly useful — they really can understand intent and produce working code for a huge variety of tasks — but they function best when <strong>given clear direction and boundaries</strong>. Left unguided, they’ll happily drift into the weeds or produce a solution that passes superficial muster but fails in edge cases.</p><p>In essence, spec-driven and agentic methods are about <strong>marrying the best of AI and human strengths</strong>: humans excel at defining problems, understanding context, and making judgment calls; AIs excel at traversing solution spaces quickly, writing boilerplate, and even coordinating tasks when set up to do so. The future of AI-assisted engineering likely lies in this middle ground — not in pure prompt-and-pray vibe coding, but in <em>augmented workflows</em> where AI amplifies human design and humans reign in AI’s excesses.</p><p>The way forward is hybrid: Sandbox phase: vibe freely, test ideas, build prototypes. Production phase: apply engineering discipline — testing, refactoring, design, security.</p><h3>Conclusion</h3><p><strong>In summary, developers and teams can evolve vibe-coded prototypes into robust systems by re-injecting all the traditional software engineering practices that might have been bypassed in the rush of AI generation: design it, test it, review it, own it.</strong></p><p>Speeding through the first draft is fine — even commendable — as long as everyone understands that <em>you then switch out of “vibe mode” and into “engineering mode.”</em> High-performing teams will likely develop an intuition for when to employ the AI fast lane and when to merge back onto the steady highway of tested, reviewed code. The end goal is the same as it’s always been: deliver software that works, is secure, and can be maintained by the team.</p><p>The tools and methods are evolving, but <strong>accountability, craftsmanship, and collaboration remain paramount in the age of AI-assisted engineering</strong>.</p><p><em>I’m excited to share I’ve written an </em><a href="https://beyond.addy.ie/"><em>AI-assisted engineering book</em></a><em> with O’Reilly. If you’ve enjoyed my writing here you may be interested in checking it out. I’ve included a number of free tips on the book site.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3f81088d5b98" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How modern browsers work]]></title>
            <link>https://medium.com/@addyosmani/how-modern-browsers-work-7e1cc7337fff?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/7e1cc7337fff</guid>
            <category><![CDATA[web-design]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[chrome]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[web-development]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Sat, 29 Nov 2025 02:40:43 GMT</pubDate>
            <atom:updated>2025-11-29T02:40:43.299Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>Note: More of my writing can be found on my </em></strong><a href="https://addyo.substack.com"><strong><em>Substack</em></strong></a><strong><em>. </em></strong><em>For those eager to dive deep into how browsers work, an </em><strong><em>excellent</em></strong><em> resource is </em><strong><em>Browser Engineering</em></strong><em> by Pavel Panchekha and Chris Harrelson (available at </em><a href="https://browser.engineering/"><em>browser.engineering</em></a><strong><em>). </em></strong><em>This article is an overview of how browsers work.</em></p><p>Web developers often treat the browser as a <strong>black box</strong> that magically transforms HTML, CSS, and JavaScript into interactive web applications. In truth, a modern web browser like Chrome (<a href="https://www.chromium.org/chromium-projects/">Chromium</a>), Firefox (<a href="https://firefox-source-docs.mozilla.org/overview/gecko.html">Gecko</a>) or Safari (<a href="https://webkit.org/">WebKit</a>) is a complex piece of software. It orchestrates networking, parses and executes code, renders graphics with GPU acceleration, and isolates content in sandboxed processes for security.</p><p>This article dives into <strong>how modern browsers work</strong> — focusing on <strong>Chromium</strong>’s architecture and internals, while noting where other engines differ. We’ll explore everything from the networking stack and parsing pipeline to the rendering process via <a href="https://www.chromium.org/blink/">Blink</a>, JavaScript engine via <a href="http://v8.dev/">V8</a>, module loading, multi-process architecture, security sandboxing, and developer tooling. The goal is a developer-friendly explanation that demystifies what happens behind the scenes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6_fgK9Z6zo6lf_6K.jpeg" /></figure><p>Let’s begin our journey through the browser’s internals.</p><h3>Networking and Resource Loading</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*R6Uz0J86zaQryR-3.png" /></figure><p>Every page load begins with the browser’s networking stack fetching resources from the web. When you enter a URL or click a link, the browser’s UI thread (running in the “<a href="https://www.chromium.org/developers/design-documents/multi-process-architecture/">browser process</a>”) kicks off a navigation request.</p><blockquote><em>The </em><strong><em>browser process</em></strong><em> is the main, controlling process that manages all other processes and the browser’s user interface. Everything that happens outside of a specific web page tab is controlled by the browser process.</em></blockquote><p>The steps include:</p><p><strong>URL parsing and security checks</strong>: The browser parses the URL to determine the scheme (http, https, etc.) and target domain. It also decides if the input is a search query or URL (in Chrome’s omnibox, for example). Security features like blocklists may be checked here to avoid phishing sites.</p><p><strong>DNS lookup</strong>: The network stack resolves the domain name to an IP address (unless it’s cached). This may involve contacting a DNS server. Modern browsers might use OS DNS services or even DNS over HTTPS (DoH) if configured, but ultimately they obtain an IP for the host.</p><p><strong>Establishing a connection</strong>: If no open connection to the server exists, the browser opens one. For HTTPS URLs, this includes a TLS handshake to securely exchange keys and verify certificates. The browser’s network thread handles protocols like TCP/TLS setup transparently.</p><p><strong>Sending the HTTP request</strong>: Once connected, an HTTP GET request (or other method) is sent for the resource. Browsers today default to HTTP/2 or HTTP/3 if the server supports it, which allows multiplexing multiple resource requests over one connection. This improves performance by avoiding the old limit of ~6 parallel connections per host (HTTP/1.1). For example, with HTTP/2 the HTML, CSS, JS, images can all be fetched concurrently on one TCP/TLS link, and with HTTP/3 (over QUIC UDP) setup latency is further reduced.</p><p><strong>Receiving the response</strong>: The server responds with an HTTP status and headers, followed by the response body (HTML content, JSON data, etc.). The browser reads the response stream. It may need to sniff the MIME type if the Content-Type header is missing or incorrect, to decide how to handle the content. For example, if a response looks like HTML but isn’t labeled as such, the browser will still try to treat it as HTML (per permissive web standards). There are security measures here too: the network layer checks Content-Type and may block suspicious MIME mismatches or disallowed cross-origin data (Chrome’s CORB — Cross-Origin Read Blocking — is one such mechanism). The browser also consults Safe Browsing or similar services to block known malicious payloads.</p><p><strong>Redirects and next steps</strong>: If the response is an HTTP redirect (e.g. 301 or 302 with a Location header), the network code will follow the redirect (after informing the UI thread) and repeat the request to the new URL. Only once a final response with actual content is obtained does the browser move on to processing that content.</p><p>All these steps happen in the network stack, which in Chromium is run in a dedicated Network Service (now typically a separate process, as part of Chrome’s “<a href="https://www.chromium.org/servicification/">servicification</a>” effort). The browser process’s network thread coordinates the low-level work of socket communication, using the OS networking APIs under the hood. Importantly, this design means the renderer (which will execute the page’s code) doesn’t directly access the network — it asks the browser process to fetch what it needs, a security win.</p><h3>Speculative Loading and Resource Optimization</h3><p>Modern browsers implement sophisticated performance optimizations in the networking stage. Chrome will proactively perform a DNS prefetch or open a TCP connection when you hover over a link or start typing a URL (using the Predictor or preconnect mechanisms) so that if you click, some latency is already shaved off. There’s also HTTP caching: the network stack can satisfy requests from the browser cache if the resource is cached and fresh, avoiding a network trip.</p><p><strong>Preload scanner operation</strong>: Chromium implements a sophisticated <a href="https://web.dev/articles/preload-scanner">preload scanner</a> that tokenizes HTML markup ahead of the main parser. When the primary HTML parser is blocked by CSS or synchronous JavaScript, the preload scanner continues examining the raw markup to identify resources like images, scripts, and stylesheets that can be fetched in parallel. This mechanism is fundamental to modern browser performance and operates automatically without developer intervention. The preload scanner cannot discover resources injected via JavaScript, making such resources likely to be loaded consecutively rather than concurrently.</p><p><strong>Early Hints (HTTP 103)</strong>: <a href="https://developer.chrome.com/docs/web-platform/early-hints">Early Hints</a> allows servers to send resource hints while generating the main response, using HTTP 103 status codes. This enables preconnect and preload hints to be sent during server think-time, potentially improving Largest Contentful Paint by several hundred milliseconds. Early Hints are only available for navigation requests and support preconnect and preload directives, but not prefetch.</p><p><strong>Speculation Rules API</strong>: <a href="https://developer.chrome.com/docs/web-platform/implementing-speculation-rules">The Speculation Rules API</a> is a recent web standard that allows defining rules to dynamically prefetch and prerender URLs based on user interaction patterns. Unlike traditional link prefetch, this API can prerender entire pages including JavaScript execution, leading to near-instant load times. The API uses JSON syntax within script elements or HTTP headers to specify which URLs should be speculatively loaded. Chrome has limits to prevent overuse, with different capacity settings based on urgency levels.</p><p><strong>HTTP/2 and HTTP/3</strong>: Most Chromium-based browsers and Firefox support HTTP/2 fully, and <a href="https://alexandrehtrb.github.io/posts/2024/03/http2-and-http3-explained/">HTTP/3</a> (based on QUIC) is also widely supported (Chrome has it enabled by default for supporting sites). These protocols improve page load by allowing concurrent transfers and reducing handshake overhead. From a developer perspective, this means you may no longer need sprite sheets or domain sharding tricks — the browser can efficiently fetch many small files in parallel on one connection.</p><p><strong>Resource prioritization</strong>: The browser also prioritizes certain resources over others. Typically, HTML and CSS are high priority (as they block rendering), scripts might be medium (or high if marked defer/async appropriately), and images maybe lower. Chromium’s network stack assigns weights and can even cancel or defer requests to prioritize what’s needed for an initial render. Developers can use <a href="https://web.dev/articles/preload-critical-assets">link rel=preload</a> and <a href="https://web.dev/articles/fetch-priority">Fetch Priority</a> to influence resource prioritization.</p><p>By the end of the networking phase, the browser has the initial HTML for the page (assuming it was an HTML navigation). At this point, Chrome’s browser process chooses a renderer process to handle the content. Chrome will often launch a new renderer process in parallel with the network request (speculatively) so that it’s ready to go when the data arrives. This renderer process is isolated (more on multi-process architecture later) and will take over for parsing and rendering the page.</p><p>Once the response is fully received (or as it streams in), the browser process commits the navigation: it signals the renderer process to take the stream of bytes and start processing the page. At this moment, the address bar updates and the security indicator (HTTPS lock, etc.) is shown for the new site. Now the action moves to the renderer process: parsing the HTML, loading sub-resources, executing scripts, and painting the page.</p><h3>Parsing HTML, CSS, and JavaScript</h3><p>When the renderer process receives the HTML content, its main thread begins to parse it according to the HTML specification. The result of parsing HTML is the DOM (Document Object Model) — a tree of objects representing the page structure. Parsing is incremental and can interleave with network reading (browsers parse HTML in a streaming fashion, so the DOM can start being built even before the entire HTML file is downloaded).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mJjhboSk8wfujVmp.png" /></figure><p><strong>HTML parsing and DOM construction</strong>: HTML parsing is defined by the HTML Standard as a error-tolerant process that will produce a DOM no matter how malformed the markup is. This means even if you forget a closing &lt;/p&gt; tag or have nested tags incorrectly, the parser will implicitly fix or adjust the DOM tree so that it’s valid. For example, &lt;p&gt;Hello &lt;div&gt;World&lt;/div&gt; will automatically end the &lt;p&gt; before the &lt;div&gt; in the DOM structure. The parser creates DOM elements and text nodes for each tag or text in the HTML. Each element is placed in a tree reflecting the nesting in the source.</p><p>One important aspect is that the HTML parser may encounter resources to fetch as it goes: for instance, encountering a &lt;link rel=”stylesheet” href=”…”&gt; will prompt the browser to request the CSS file (on the network thread), and encountering an &lt;img src=”…”&gt; will trigger an image request. These happen in parallel to parsing. The parser can keep going while those loads occur, with one big exception: scripts.</p><p><strong>Handling &lt;script&gt; tags</strong>: If the HTML parser comes across a &lt;script&gt; tag, it pauses parsing and must execute the script before continuing (by default). This is because scripts can use document.write() or other DOM manipulation that can alter the page structure or content that’s still coming in. By executing immediately at that point, the browser preserves the correct order of operations relative to the HTML. The parser therefore hands off the script to the JavaScript engine for execution, and only when the script finishes (and any DOM changes it did are applied) can HTML parsing resume. This script execution blocking behavior is why including large &lt;script&gt; files in the head can slow down page rendering — the HTML parsing can’t continue until the script is downloaded and run.</p><p>However, developers can modify this behavior with attributes: adding <a href="https://web.dev/articles/efficiently-load-third-party-javascript">defer or async</a> to a &lt;script&gt; tag (or using modern ES module scripts) changes how the browser handles it. With async, the script file is fetched in parallel and executed as soon as it’s ready, without pausing HTML parsing (the parse doesn’t wait, and the script doesn’t guarantee execution in original order relative to other async scripts). With defer, the script is fetched in parallel but execution is deferred until the HTML parsing is done (and will execute in the original order at that later time). In both cases, the parser isn’t blocked waiting on the script, which is generally better for performance. ES6 modules (using &lt;script type=”module”&gt;) are automatically deferred as well (and they can also use import statements — we’ll cover module loading separately). By using these techniques, the browser can continue building the DOM without long pauses, making pages load faster.</p><p><strong>CSS Parsing and the CSSOM</strong>: Alongside HTML, CSS text must be parsed into a structure the browser can work with — often called the CSSOM (CSS Object Model). The <a href="https://web.dev/articles/critical-rendering-path/constructing-the-object-model">CSSOM</a> is essentially a representation of all the styles (rules, selectors, properties) that apply to the document. The browser’s CSS parser reads CSS files (or &lt;style&gt; blocks) and turns them into a list of CSS rules (and lots of bloom filters etc to speed up style resolution). Then, as the DOM is being constructed (or once both DOM and CSSOM are ready), the browser will compute the style for each DOM node. This step is usually called style resolution or style calculation. The browser combines the DOM and CSSOM to determine, for each element, which CSS rules apply and what the final computed styles are (after applying the cascade, inheritance, and default styles). The output is often conceptualized as an association of each DOM node with a computed style (the resolved, final CSS properties for that element, e.g. an element’s color, font, size, etc.).</p><p>It’s worth noting that even without any author CSS, every element has default browser styles (the user-agent stylesheet). For example, a &lt;h1&gt; has a default font-size and margin in practically all browsers. The browser’s built-in style rules are applied with the lowest priority, and they ensure some reasonable default presentation. Developers can view computed styles in DevTools to see exactly what CSS properties an element ends up with. The style calculation step uses all applicable styles (user agent, user styles, author styles) to finalize each element’s styling.</p><p><strong>Render-blocking behavior</strong>: While HTML parsing can proceed without CSS fully loaded, there is a <a href="https://web.dev/learn/performance/understanding-the-critical-path">render-blocking relationship</a>: browsers typically wait to perform the first render until CSS is loaded (for CSS in the &lt;head&gt;). This is because applying an incomplete stylesheet could flash unstyled content. In practice, if a &lt;script&gt; that is not marked async/defer appears before a CSS &lt;link&gt; in HTML, it will additionally wait for the CSS to load before executing the script (since scripts may query style information via DOM APIs). As a rule of thumb, put stylesheet links in the head (they block rendering but are needed early) and put non-critical or large scripts with defer/async or at the bottom so they don’t delay DOM parsing.</p><p>Now the browser has (1) the DOM constructed from HTML, (2) the CSS rules parsed (CSSOM), and (3) the computed styles for each DOM node. These together form the basis for the next stage: layout. But before moving on, we should consider the JavaScript side in more detail — specifically how the JS engine (V8 in Chrome’s case) executes code. We touched on script blocking, but what happens when the JS runs? We’ll dedicate a later section to the internals of V8 and JS execution. For now, assume that as scripts run, they might modify the DOM or CSSOM (for example, calling document.createElement or setting element styles). The browser may have to respond to those changes by recalculating styles or layout as needed (which can incur performance costs if done repeatedly). The initial run of scripts during parsing often includes things like setting up event handlers, or maybe manipulating the DOM (e.g. templating). After that, the page is usually fully parsed and we move into layout and rendering.</p><h3>Styling and Layout</h3><p>At this stage, the browser’s renderer process knows the structure of the DOM and each element’s computed style. The next question is: where do all these elements go on the screen? How big are they? This is the job of layout (also known as “reflow” or “layout calculation”). In this phase, the browser calculates the geometry of each element — their size and position — according to the CSS rules (flow, box model, flexbox or grid, etc.) and the DOM hierarchy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*V1TGGYhsMnnkINGy.png" /></figure><p><strong>Layout tree construction</strong>: The browser walks the DOM tree and generates a layout tree (sometimes called the render tree or frame tree). The layout tree is similar to the DOM tree in structure, but it omits non-visual elements (e.g. script or meta tags don’t produce boxes) and may split some elements into multiple boxes if needed (for example, a single HTML element that is flowed across multiple lines might correspond to multiple layout boxes). Each node in the layout tree holds the computed style for that element and has information like the node’s content (text or image) and computed properties that affect layout (like width, height, padding, etc.).</p><p>During layout, the browser computes the exact position (x, y coordinates) and size (width, height) for each element’s box. This involves algorithms defined by CSS specifications: for example, in a normal document flow, block-level elements stack top-to-bottom, each taking full width by default, whereas inline elements flow within lines and cause line breaks as needed. Modern layout modes like <a href="https://web.dev/learn/css/flexbox">flexbox</a> or <a href="https://web.dev/learn/css/grid">grid</a> have their own algorithms. The engine has to consider font metrics to break lines (so text layout involves measuring text runs), and it must handle margins, padding, border, etc. There are many edge cases (e.g. margin collapsing rules, floats, absolutely positioned elements that are removed from flow, etc.), making layout a surprisingly complex process. Even a “simple” top-to-bottom layout has to figure out line breaks in text which depend on available width and font sizes. Browser engines have dedicated teams and many years of development to handle layout accurately and efficiently.</p><p>Some details about the layout tree:</p><ul><li>Elements with display:none are omitted entirely from the layout tree (they don’t produce any box). In contrast, elements that are simply not visible (e.g. visibility:hidden) do get a layout box (taking up space), just not painted later.</li><li>Pseudo-elements like ::before or ::after that generate content are included in the layout tree (since they do have visual boxes).</li><li>The layout tree nodes know their geometry. For example, a &lt;p&gt; element’s layout node will know its position relative to the viewport and its dimensions, and have children for each line or inline box inside it.</li></ul><p><strong>Layout calculation</strong>: Layout is typically a recursive process. Starting from the root (the &lt;html&gt; element), the browser computes the size of the viewport (for &lt;html&gt;/&lt;body&gt;) and then lays out child elements within it, and so on. Many elements’ sizes depend on their children or parent (e.g. a container might expand to fit children, or a child might be 50% of its parent’s width). The layout algorithm often has to do multiple passes for things like floats or for certain complex interactions, but generally it proceeds in one direction (top-down) with possible backtracking if needed.</p><p>By the end of this stage, each element’s position and size on the page is known. We can now conceptually think of the page as a bunch of boxes (with text or images inside). But we still haven’t actually drawn anything on the screen yet — that’s the next step, painting.</p><p>However, one key concept: layout can be an expensive operation, especially if done repeatedly. If JavaScript later changes the size of an element or adds content, it can force a relayout of some or all of the page. Developers often hear advice about avoiding layout thrashing (like reading layout info in JS right after modifying DOM, which can force synchronous recalculation). The browser tries to optimize by noting what parts of the layout tree are “dirty” and only recomputing those. But worst-case, changes high up in the DOM could require recalculating the entire layout for large pages. This is why costly style/layout operations should be minimized for better performance.</p><p><strong>Style and layout recap</strong>: To summarize, from HTML and CSS the browser builds:</p><ul><li>DOM tree — structure and content</li><li>CSSOM — parsed CSS rules</li><li>Computed Styles — the result of matching CSS rules to each DOM node</li><li>Layout tree — DOM tree filtered to visual elements, with geometry for each node</li></ul><p>Each stage builds on the last. If any stage changes (e.g. if a script alters the DOM or modifies a CSS property), the subsequent stages may need to update. For example, if you change a CSS class on an element, the browser may recalc style for that element (and children if inheritance changes), then might have to redo layout if that style change affects geometry (say display or size), then would have to repaint. This chain means layout and paint are dependent on up-to-date style, and so on. We’ll discuss performance implications of this in the DevTools section (as the browser provides tools to see when these steps occur and how long they take).</p><p>With layout done, we move to the next major phase: painting.</p><h3>Painting, Compositing, and GPU Rendering</h3><p>Painting is the process of taking the structured layout information and actually producing pixels on the screen. In traditional terms, the browser would traverse the layout tree and issue drawing commands for each node (“draw background, draw text, draw image at these coordinates”). Modern browsers still conceptually do this, but they often split the work into multiple stages and leverage the GPU for efficiency.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*To4JDXzFAFcLiqJp.png" /></figure><p><strong>Painting / Rasterization</strong>: On the renderer’s main thread, after layout, Chrome generates paint records (or a display list) by walking the layout tree. This is basically a list of drawing operations with their coordinates, much like an artist planning how to paint the scene: e.g. “draw rect at (x,y) with width W and height H and fill color blue, then draw text ‘Hello’ at (x2,y2) with font XYZ, then draw an image at …” and so on. This list is in the correct z-index order (so that overlapping elements paint correctly). For example, if an element has a higher z-index, its paint commands will come later (on top of) lower z-index content. The browser must consider stacking contexts, transparency, etc. to get the right ordering.</p><p>In the past, browsers might have simply drawn each element directly to the screen in order. But that approach can be inefficient if parts of the page change (you’d have to repaint everything). Modern browsers instead often record these drawing commands and then use a compositing step to assemble the final image, especially when using GPU acceleration.</p><p><strong>Layering and compositing</strong>: Compositing is an optimization where the page is split into several layers that can be handled independently. For example, a positioned element with a CSS transform or an animation might get its own layer. Layers are like separate “scratch canvases” — the browser can rasterize (draw) each layer separately, and then the compositor can blend them together on the screen, often using the GPU.</p><p>In Chromium’s pipeline, after paint records are generated, there’s a step to build the layer tree (this corresponds to which elements are on which layer). Some layers are created automatically (e.g. a video element, or a canvas, or elements with certain CSS will be promoted to layers), and developers can hint by using will-change or CSS properties like transform to get a layer. The reason layers are helpful is that movement or opacity changes on a layer can be composited (i.e. just that layer re-rendered or moved) without re-painting the whole page. Too many layers, however, can be memory-heavy and add overhead, so browsers choose carefully.</p><p>After determining layers, Chrome’s main thread hands off to the compositor thread. The compositor thread runs in the renderer process but separate from the main thread (so it can keep working even if the main JS thread is busy, which is great for smooth scrolling and animations). The compositor thread’s job is to take the layers, rasterize them (convert the drawings into actual pixel bitmaps), and compose them into frames.</p><p><strong>Rasterization with GPU assistance</strong>: Raster work can also be distributed. In Chrome, the compositor thread breaks layers into smaller tiles (think 256x256 or 512x512 pixel chunks, which are often larger when GPU raster is on, almost always). It then dispatches these to several raster worker threads (which may even run across multiple CPU cores) for concurrent rasterization. Each raster worker takes a tile — essentially a list of drawing commands for that region of a layer — and produces a bitmap (pixel data). Importantly, Skia (Chrome’s graphics library) can use the CPU or GPU to rasterize; in Chrome’s case, these raster threads typically use CPU to render the pixels and then upload them to GPU memory. Firefox’s newer WebRender takes a different approach we’ll mention later. The rasterized tiles are stored in GPU memory as textures. Once all needed tiles are drawn, the compositor thread has essentially a set of textured layers ready.</p><p>The compositor then assembles a compositor frame — basically a message to the browser process that includes all the quads (tiles of layers) that make up the screen, their positions, etc. This compositor frame is submitted via IPC back to the browser process, where ultimately the browser’s GPU process (a separate process in Chrome for accessing GPU) will take these and display them. The browser process’s own UI (like the tab bar) is also drawn via compositor frames, and they all get mixed in the final step. The GPU process receives the frames, and uses the GPU (via OpenGL/DirectX/Metal etc.) to composite them — basically drawing each texture at the right place on screen, applying transforms, etc. very fast. The result is the final image you see displayed.</p><p>The advantage of this pipeline is apparent when you scroll or animate. For example, scrolling a page mostly just changes the viewport on a larger page texture. The compositor can just shift the layer positions and ask the GPU to redraw the new portion coming into view, without the main thread having to repaint everything. If an animation is just a transform (say moving an element that is its own layer), the compositor thread can update that element’s position each frame and produce new frames without involving the main thread or re-running style and layout. This is why animations that are “compositing-only” (changing transform or opacity, which don’t trigger layout) are recommended for better performance — they can run at 60 FPS smoothly even if the main thread is busy. In contrast, animating something like height or background-color might force re-layout or re-paint each frame, which janks if the main thread can’t keep up.</p><p>To put it succinctly, Chrome’s rendering pipeline is: DOM → style → layout → paint (record display items) → layerize → raster (tiles) → composite (GPU). Firefox’s pipeline is conceptually similar up to the display list stage, but with WebRender it skips explicit layer construction and instead sends a display list to the GPU process, which then handles almost all drawing using GPU shaders (more on this in the comparison section). WebKit (Safari) also uses a multi-threaded compositor and GPU rendering via “CALayers” on macOS. All modern engines thus take advantage of GPUs for rendering, especially for compositing and rasterizing graphics-intensive parts, to achieve high frame rates and offload work from the CPU.</p><p>Before moving on, let’s discuss the GPU’s role in more detail. In Chromium, the GPU process is a separate process whose job is to interface with the graphics hardware. It receives drawing commands (mostly high-level, like “draw these textures at these coords”) from all renderer compositors and also the browser UI. It then translates that into actual GPU API calls. By isolating it in a process, a buggy GPU driver that crashes won’t take down the whole browser — only the GPU process, which can be restarted. Also, it provides a sandbox boundary (since GPUs process potentially untrusted content like canvas drawing, WebGL, etc. there have been security bugs in drivers — running them out-of-process mitigates risk).</p><p>The result of the compositing is finally sent to the display (the OS window or context the browser is running in). For each animation frame (target 60fps or 16.7ms per frame for smooth results), the compositor aims to produce a frame. If the main thread is busy (say JavaScript took a long time), the compositor might skip frames or can’t update, leading to visible jank. Developer tools can show dropped frames in the performance timeline. Techniques like requestAnimationFrame align JS updates to frame boundaries to help with smooth rendering.</p><p>In summary, the browser’s rendering engine carefully breaks down the page content and styles into a set of geometry (layout) and drawing instructions, then uses layers and GPU compositing to efficiently turn that into the pixels you see. This complex pipeline is what enables the rich graphics and animations on the web to run at interactive frame rates. Next, we will peek into the JavaScript engine to understand how the browser executes scripts (which we’ve so far treated as a black box).</p><h3>Inside the JavaScript Engine (V8)</h3><p>JavaScript drives the interactive behavior of web pages. In Chromium browsers, the V8 engine executes JavaScript (and WebAssembly). Understanding V8’s workings can help developers write performant JS. While an exhaustive deep-dive would be book-length, we’ll focus on the key stages of the JS execution pipeline: parsing/compiling the code, executing it, and managing memory (garbage collection). We’ll also note how V8 handles modern features like Just-In-Time (JIT) compilation tiers and ES modules.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lyqRQ9JhA_r2SCkp.png" /></figure><h3>Modern V8 Parsing and Compilation Pipeline</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4vMxdmHdZmCUfR4R.jpeg" /></figure><p><strong>Background compilation</strong>: Starting with Chrome 66, V8 compiles JavaScript source code on a background thread, reducing the amount of time spent compiling on the main thread by between 5% to 20% on typical websites. Since version 41, Chrome has supported parsing of JavaScript source files on a background thread via V8’s StreamedSource API. V8 can start parsing JavaScript source code as soon as the first chunk is downloaded from the network and continue parsing in parallel while streaming the file. Almost all script compilation occurs on background threads, with only short AST internalization and bytecode finalization steps happening on the main thread just before script execution. Currently, top-level script code and immediately invoked function expressions are compiled on background threads, while inner functions are still compiled lazily on the main thread when first executed.</p><p><strong>Parsing and bytecode</strong>: When a &lt;script&gt; is encountered (either during HTML parse or loaded later), V8 first parses the JavaScript source code. This produces an Abstract Syntax Tree (AST) representation of the code. The preparser is a copy of the parser that does the bare minimum needed to skip over functions. It verifies that functions are syntactically valid and produces all information needed for outer functions to be compiled correctly. When a preparsed function is later called, it is fully parsed and compiled on-demand.</p><p>Rather than interpreting directly from the AST, V8 uses a bytecode interpreter called Ignition (introduced in 2016). Ignition compiles the JavaScript into a compact bytecode format, which is essentially a sequence of instructions for a virtual machine. This initial compilation is quite fast and the bytecode is fairly low-level (Ignition is a register-based VM). The goal is to start executing the code quickly with minimal upfront cost (important for page load times).</p><p><strong>AST internalization process</strong>: AST internalization involves allocating literal objects (strings, numbers, object-literal boilerplate) on the V8 heap for use by generated bytecode. To enable background compilation, this process was moved later in the compilation pipeline, after bytecode compilation, requiring modifications to access raw literal values embedded in the AST instead of internalized on-heap values.</p><p><strong>Explicit Compile Hints</strong>: V8 has introduced a new feature called “<a href="https://v8.dev/blog/explicit-compile-hints">Explicit Compile Hints</a>” which allows developers to instruct V8 to parse and compile code immediately on load through eager compilation. Files with this hint are compiled on background threads, whereas deferred compilation happens on the main thread. Experiments with popular web pages showed performance improvements in 17 out of 20 cases, with an average 630ms reduction in foreground parse and compile times. Developers can add explicit compile hints to JavaScript files using special comments to enable eager compilation on background threads for critical code paths.</p><p><strong>Scanner and parser optimizations</strong>: V8’s scanner has been significantly optimized, resulting in improvements across the board: single token scanning improved by roughly 1.4×, string scanning by 1.3×, multiline comment scanning by 2.1×, and identifier scanning by 1.2–1.5× depending on identifier length.</p><p>When the script runs, Ignition interprets the bytecode, executing the program. Interpretation is generally slower than optimized machine code, but it allows the engine to start running and also collect profiling information about the code’s behavior. As the code runs, V8 gathers data on how it’s being used: types of variables, which functions are called frequently, etc. This information will be used to make the code run faster in subsequent steps.</p><h3>JIT Compilation Tiers</h3><p>V8 doesn’t stop at interpretation. It employs multiple tiers of Just-In-Time compilers to accelerate hot code. The idea is to spend more compilation effort on code that runs a lot, to make it faster, while not wasting time optimizing code that runs only once.</p><ol><li><strong>Ignition</strong> (interpreting the bytecode).</li><li><strong>Sparkplug</strong>: V8’s baseline JIT called Sparkplug (launched around 2021). Sparkplug takes the bytecode and compiles it to machine code quickly, without heavy optimizations. This yields native code that is faster than interpretation but Sparkplug doesn’t do deep analysis — it’s meant to be almost as quick as the interpreter to start, but produce code that runs a bit faster.</li><li><strong>Maglev</strong>: In 2023, V8 introduced Maglev, a mid-tier optimizing compiler that is now actively deployed. Maglev generates code nearly 20 times slower than Sparkplug but 10 to 100 times faster than TurboFan, effectively bridging the gap for functions that are moderately hot but not hot enough for TurboFan optimization. Maglev comes into play for functions that are somewhat hot but not hot enough for TurboFan, or when TurboFan’s compilation would be too costly. As of Chrome M117, Maglev can handle many cases, resulting in faster startup for web apps that spend time in “warm” code (not cold, not super hot) by bridging the gap between baseline and highest-tier JIT.</li><li><strong>TurboFan</strong>: As functions or loops get executed many times, V8 will engage its most powerful optimizing compiler. TurboFan takes the code and uses the collected type feedback to generate highly optimized machine code, applying advanced optimizations (inlining functions, eliding bounds checks, etc.). This optimized code can run much faster if the assumptions hold.</li></ol><p>So V8 now effectively has four execution tiers: Ignition interpreter, Sparkplug baseline JIT, Maglev optimizing JIT, and TurboFan optimizing JIT. This is analogous to how Java’s HotSpot VM has multiple JIT levels (C1 and C2). The engine can dynamically decide which functions to optimize and when, based on execution profiles. If a function suddenly is called a million times, it will likely end up TurboFan-optimized for maximum speed.</p><p>Intel has also developed <a href="https://community.intel.com/t5/Blogs/Tech-Innovation/Client/Profile-Guided-Tiering-in-the-V8-JavaScript-Engine/post/1679340">Profile-Guided Tiering</a> that enhances V8’s efficiency, leading to approximately 5% improvement on Speedometer 3 benchmarks. Recent V8 updates include static roots optimization, which allows accurate prediction of memory addresses at compile time for commonly used objects, significantly improving access speed.</p><p>One challenge with JIT optimization is that JavaScript is dynamically typed. V8 might optimize code under certain assumptions (e.g. this variable is always an integer). If a later call violates those assumptions (say the variable becomes a string), the optimized code is invalid. V8 then performs a deoptimization: it falls back to a less optimized version (or re-generates code with new assumptions). This mechanism relies on “inline caches” and type feedback to quickly adapt. The existence of deopt means sometimes peak performance isn’t sustained if your code has unpredictable types, but generally V8 tries to handle typical patterns (like a function consistently being passed the same type of object).</p><h3>Bytecode Flushing and Memory Management</h3><p>V8 implements bytecode flushing where if a function remains unused after multiple garbage collections, its bytecode will be reclaimed. When executed again, the parser uses previously stored results to regenerate the bytecode more quickly. This mechanism is crucial for memory management but can lead to parsing inconsistencies in edge cases.</p><p><strong>Memory Management (Garbage Collection)</strong>: V8 automatically manages memory for JS objects using a garbage collector. Over the years, V8’s GC has evolved into what’s known as the Orinoco GC, which is a generational, incremental, and concurrent garbage collector. Key points:</p><ul><li><strong>Generational</strong>: V8 segregates objects by age. New objects are allocated in the young generation (or “nursery”). These are collected frequently with a very fast scavenging algorithm (copying live objects to a new space and reclaiming the rest). Objects that survive enough cycles get promoted to the old generation.</li><li><strong>Mark-and-sweep/compact</strong>: For the old generation, V8 uses a mark-and-sweep collector with compaction. This means it will occasionally stop the world (stop JS execution briefly), mark all reachable objects (tracing from roots like the global object), then sweep to reclaim memory from unreferenced objects. It may also compact memory (moving objects to reduce fragmentation). However, Orinoco has made much of the marking concurrent — it can do a lot of the marking on a background thread while JS is still running, to minimize pause times.</li><li><strong>Incremental GC</strong>: V8 performs garbage collection in small slices rather than one big pause when possible. This incremental approach spreads work out to avoid jank. For example, it can interleave a bit of marking work between script executions, using idle time.</li><li><strong>Parallel GC</strong>: On multi-core machines, V8 can perform parts of GC (like marking or sweeping) in parallel threads as well.</li></ul><p>The net effect is that the V8 team has managed to drastically reduce GC pause times over the years, making garbage collection mostly unnoticeable even in large applications. Minor GCs (new object scavenge) usually happen very fast. Major GCs (old gen) are rarer and mostly concurrent now. If you open Chrome’s Task Manager or DevTools Memory panel, you might see V8’s heap broken into “Young space” and “Old space” reflecting this generational design.</p><p>For developers, this means manual memory management isn’t needed, but you should still be mindful: e.g. avoid creating tons of short-lived objects in tight loops (though V8 is quite good at handling short-lived objects) and be aware that holding onto large data structures will keep them around in memory. Tools like DevTools can force a garbage collection or record memory profiles to see what is using memory.</p><p><strong>V8 and Web APIs</strong>: It’s worth mentioning that V8 covers the core JavaScript language and runtime (execution, standard JS objects, etc.), but many “browser APIs” (like DOM methods, alert(), network XHR/fetch, etc.) are not part of V8 itself. Those are provided by the browser and are exposed to JS via bindings. For instance, when you call document.querySelector, under the hood it enters the engine’s binding to the C++ DOM implementation. V8 handles calling into C++ and getting results back, and there is a lot of machinery to make this boundary fast (Chrome uses an IDL to generate efficient bindings).</p><p>Having covered how the browser fetches resources, parses HTML/CSS, computes layout, paints with the GPU, and runs JS, we now have a picture of the entire process of loading and rendering a page. But there’s more to explore: how ES modules are handled (since modules involve their own loading mechanism), how the browser’s multi-process architecture is organized, and how security features like sandboxing and site isolation work.</p><h3>Module Loading and Import Maps</h3><p><a href="https://v8.dev/features/modules">JavaScript modules</a> (ES6 modules) introduce a different loading and execution model compared to classic &lt;script&gt; tags. Instead of a big script file that might create globals, modules are files that explicitly import/export values. Let’s see how browsers (and specifically V8 in Chrome) load modules and how features like dynamic import() and import maps come into play.</p><p><strong>Static module imports</strong>: When the browser encounters a &lt;script type=”module” src=”main.js”&gt;, it treats main.js as a module entry point. The loading process works as follows: the browser will fetch main.js, then parse it as an ES module. During parsing, it will find any import statements (e.g. import { foo } from ‘./utils.js’;). Rather than executing code immediately, the browser constructs a module dependency graph. It will initiate fetching of any imported modules (utils.js in this case), and recursively, each of those modules is parsed for their imports, fetched, and so on. This happens asynchronously. Only once the entire graph of modules is fetched and parsed can the browser evaluate the modules. Module scripts are deferred by nature — the browser doesn’t execute the module code until all dependencies are ready. Then it executes them in dependency order (ensuring that if module A imports B, B runs first).</p><p>This static import process is why ES modules can’t be loaded from file:// in some cases unless allowed, and why they require CORS by default for cross-origin scripts — the browser is actively linking and loading multiple files, not just dropping a &lt;script&gt; into the page.</p><p><strong>Dynamic import()</strong>: In addition to static import statements, ES2020 introduced import(moduleSpecifier) as an expression. This allows code to load a module on the fly (returning a promise that resolves to the module exports). For example, you might do const module = await import(‘./analytics.js’) in response to a user action, thereby code-splitting your application. Under the hood, import() triggers the browser to fetch the requested module (and its dependencies, if not already loaded), then instantiate and execute it, and resolve the promise with the module namespace object. V8 and the browser coordinate here: the browser’s module loader handles fetching and parsing, V8 handles the compilation and execution once ready. Dynamic import is powerful because it can be used in non-module scripts too (e.g. an inline script can dynamically import a module). It essentially gives the developer control to load JS on demand. The difference from a static import is that static imports are resolved ahead of time (before any module code runs, the entire graph is loaded), whereas dynamic import behaves more like loading a new script at runtime (except with module semantics and promises).</p><p><strong>Import maps</strong>: One challenge with ES modules in the browser was the module specifiers. In Node or bundlers, you often import by package name (e.g. import { compile } from ‘react’). On the web, without a bundler, ‘react’ is not a valid URL — the browser would treat it as a relative path (which would fail). This is where Import Maps come in. An import map is a JSON configuration that tells the browser how to resolve module specifiers to real URLs. It’s provided via a &lt;script type=”importmap”&gt; tag in HTML. For example, an import map might say that the specifier “react” maps to “https://cdn.example.com/react@19.0.0/index.js&quot; (some full URL to the actual script). Then, when any module does import ‘react’, the browser uses the map to find the URL and loads that. Essentially, import maps allow “bare” specifiers (like package names) to work on the web by mapping them to CDN URLs or local paths.</p><p>Import maps have been a game-changer for unbundled development. Since 2023, import maps are supported in all major browsers (Chrome 89+, Firefox 108+, Safari 16.4+ — all three engines). They are especially useful for local development or simple apps where you want to use modules without a build step. For production, large apps often still bundle for performance (to reduce the number of requests), but as browsers and HTTP/2/3 improve, serving many small modules becomes more viable.</p><p>The module loader in the browser thus consists of: a module map (tracking what’s been loaded), possibly an import map (for custom resolution), and the fetching/parsing logic. Once fetched and compiled, module code executes in strict mode and with its own top-level scope (no leaking to window unless explicitly attached). The exports are cached so if another module imports the same module later, it doesn’t re-run it (it reuses the already evaluated module record).</p><p>One more aspect to mention is that ES modules, unlike scripts, defer execution and also execute in order for a given graph. If main.js imports util.js and util.js imports dep.js, the evaluation order will be: dep.js first, then util.js, then main.js (depth-first, post-order). This deterministic order can avoid the need for things like DOMContentLoaded in some cases, since by the time your main module runs, all its imports are loaded and executed.</p><p>From V8’s perspective, modules are handled by the same compilation pipeline, but they create separate ModuleRecords. The engine ensures that a module’s top-level code only runs once all dependencies are ready. V8 also has to deal with cyclic module imports (which are allowed and can lead to partially initialized exports). The details are per spec — but essentially, the engine will create all module instances, then resolve cycles by giving them placeholders, and then execute in an order that respects dependencies (the spec algorithm is the “DAG” topological sort of the module graph).</p><p>In summary, module loading in browsers is a coordinated dance between the network (fetching module files), the module resolver (using import maps or standard URL resolution), and the JS engine (compiling and evaluating modules in the correct order). It’s more involved than old &lt;script&gt; loading, but results in a more modular and maintainable code structure. For developers, the key takeaways are: use modules to organize code, use import maps if you want bare imports, and know that you can dynamically load modules when needed via import(). The browser will handle the heavy lifting of making sure everything executes in the right sequence.</p><p>Now that we’ve covered how a single page’s internals work, let’s zoom out and examine the browser architecture that allows multiple pages, tabs, and web apps to all run simultaneously without interfering with each other. This brings us to the multi-process model.</p><h3>Browser Multi-Process Architecture</h3><p>Modern browsers (Chrome, Firefox, Safari, Edge, etc.) all use a multi-process architecture for stability, security, and performance isolation. Instead of running the entire browser as one giant process (which was how early browsers worked), different aspects of the browser run in different processes. Chrome was a pioneer of this approach in 2008, and others followed suit in various forms. Let’s focus on Chromium’s architecture and note differences in Firefox and Safari.</p><p>In Chromium (Chrome, Edge, Brave, etc.), there is one <strong>Browser Process</strong> that is central. This browser process is responsible for the UI (the address bar, bookmarks, menus — all the browser chrome) and for coordinating high-level tasks like resource loading and navigation. When you open Chrome and see one entry in your OS task manager, that’s the browser process. It’s also the parent that spawns other processes.</p><p>Then, for each tab (and sometimes for each site in a tab), Chrome creates a <strong>Renderer Process</strong>. A renderer process runs the Blink rendering engine and V8 JS engine for the content of that tab. In general, each tab gets at least one renderer process.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*l7T7M_-GRo-5FK6f.jpeg" /></figure><p>If you have multiple unrelated sites open, they’ll be in separate processes (Site A in one, Site B in another, etc.). Chrome even isolates cross-origin iframes into separate processes (more on that in site isolation). The renderer process is sandboxed and cannot directly access your file system or network arbitrarily — it has to go through the browser process for those privileged operations.</p><p>Other key processes in Chrome include:</p><ul><li><strong>GPU Process</strong>: a process dedicated to communicating with the GPU (as described earlier). All rendering and compositing requests from renderers go to the GPU process, which actually issues graphics API calls. This process is sandboxed and separate so that a GPU crash doesn’t take down renderers.</li><li><strong>Network Process</strong>: (In older Chrome versions the network was a thread in browser process, but now it’s often a separate process through “servicification”). This process handles network requests, DNS, etc. and can be sandboxed separately.</li><li><strong>Utility Processes</strong>: these are for various services (like audio playback, image decoding, etc.) that Chrome may offload.</li><li><strong>Plugin Process</strong>: in the era of Flash and NPAPI plugins, plugins ran in their own process. Flash is deprecated now, so this is less relevant, but the architecture remains ready for plugins to not run in the main browser process.</li><li><strong>Extension Processes</strong>: Chrome extensions (which are essentially scripts that can act on web pages or the browser) run in separate processes as well, isolated from websites for security.</li></ul><p>A simplified view is: one Browser process coordinates multiple Renderer processes (one per tab or per site instance), plus one GPU process and a few others for services. Chrome’s task manager (Shift+Esc on Windows or via More Tools &gt; Task Manager) will actually list each process type and its memory usage.</p><p><strong>Benefits of Multi-Process</strong>: The primary benefits are:</p><ul><li><strong>Stability</strong>: If a web page (renderer process) crashes or leaks memory, it doesn’t crash the whole browser — you can close that tab and the rest stays alive. In one-process browsers, a single bad script could tear down everything. Chrome can show the “Aw, Snap” error for a single tab when its process dies, and you can reload it independently.</li><li><strong>Security (Sandboxing)</strong>: By running web content in a restricted process, the browser can limit what that code can do on your system. Even if an attacker finds a vulnerability in the rendering engine, they are trapped in the sandbox — the renderer process typically cannot read your files or arbitrarily open network connections or launch programs. It must request the browser process for things like file access, which can be validated or denied. This sandbox is enforced at the OS level (using job objects, seccomp filters, etc. depending on platform).</li><li><strong>Performance Isolation</strong>: Intensive work in one tab (a heavy webapp or an infinite loop) is mostly confined to that tab’s renderer process. Other tabs (different process) can remain responsive because their processes aren’t blocked. Also, the OS can schedule processes on different CPU cores — so two heavy pages can run in parallel on a multi-core system better than if they were threads of one process.</li><li><strong>Memory segmentation</strong>: Each process has its own address space, so memory is not shared. This prevents one site from snooping on data of another and also means when a tab is closed, the OS can reclaim all memory from that process efficiently. The downside is some overhead due to duplicated resources and processes (each renderer loads its own copy of the JS engine, etc.).</li></ul><p><strong>Site Isolation</strong>: Initially, Chrome’s model was one process per tab. Over time they evolved it to one process per site (especially after Spectre — see next section on security). As of 2024, site isolation is enabled by default for 99% of Chrome users across desktop platforms, with Android support continuing to be refined. This means if you have two tabs both open to example.com, Chrome might decide to use one process for both (to save memory, because they’re the same site and thus less risky to put together). But a tab with example.com and an iframe of evil.com would by default put evil.com’s iframe in a separate process from the parent page (to protect the example.com data). This enforcement is what Chrome calls “Strict Site Isolation” (launched around Chrome 67 as a default). Site isolation causes Chrome to use 10–13% more system resources due to increased process creation, but provides crucial security benefits.</p><p>Firefox’s architecture, called <a href="https://blog.mozilla.org/addons/2016/04/11/the-why-of-electrolysis/">Electrolysis</a> (e10s), was historically one content process for all tabs (for many years Firefox was single-process and only enabled a few content processes around 2017). As of 2021, Firefox uses multiple content processes (by default 8 for web content). With <a href="https://blog.mozilla.org/security/2021/05/18/introducing-site-isolation-in-firefox/">Project Fission</a> (site isolation), Firefox is moving toward isolating sites similarly — it can spin up new processes for cross-site iframes, and in Firefox 108+ they enabled site isolation by default, increasing the number of processes to potentially one per site like Chrome. Firefox also has a GPU process (for WebRender and compositing) and a separate networking process, similar to Chrome’s split. So in practice, Firefox now has a very Chrome-like model: a parent process, a GPU process, a network process, a few content (renderer) processes, and some utility processes (for extensions, media decoding, etc. — e.g. a media plugin can run isolated).</p><p>Safari (WebKit) likewise moved to a multi-process model (WebKit2) where each tab’s content is in a separate WebContent process and a central UI process controls them. Safari’s WebContent processes are also sandboxed and cannot directly access devices or files without going through the UI process. Safari also has a networking process that is shared (and possibly other helpers). So while implementations differ, the concept is consistent: isolate each webpage’s code in its own sandboxed environment.</p><p>One important point is inter-process communication (IPC): How do these processes talk to each other? Browsers use IPC mechanisms (on Windows, often named pipes or other OS IPC; on Linux, maybe Unix domain sockets or shared memory; Chrome has its own IPC library Mojo). For example, when a network response arrives in the Network process, it needs to be delivered to the correct Renderer process (via the Browser process coordinating). Similarly, when you do a DOM fetch(), the JS engine will call into a network API which sends a request to the Network process and so on. IPC adds complexity, but browsers optimize heavily (e.g. using shared memory for transferring large data like images efficiently, and posting asynchronous messages to avoid blocking).</p><p><strong>Process Allocation Strategies</strong>: Chrome doesn’t always create a brand new process for every single tab — there are limits (particularly on devices with low memory, it may reuse processes for same-site tabs). Chrome will reuse an existing renderer if you open another tab to the same site, to conserve memory (this is why sometimes two tabs of the same site share process). It also has a limit on total processes (which can scale based on RAM). When the limit is hit, it might start putting multiple unrelated sites in one process, though it tries hard to avoid mixing sites if site isolation is enabled. On Android, Chrome uses fewer processes because of the memory constraints (often a max of 5–6 processes for content).</p><p>One more concept in Chromium is <strong>servicification</strong>: splitting browser components into services that could run in separate processes. For example, the Network Service was made a separate module that can run out-of-process. The idea is modularity — powerful systems can run each service in its own process, whereas constrained devices might consolidate some services back into one process to save overhead. Chrome can decide at runtime or build time how to deploy these services. As noted in the snippet, on high-end it might split everything (UI, net, GPU, etc. all separate), and on low-end (Android) it might combine browser &amp; network in one process to cut down overhead.</p><p>The takeaway: Chromium’s architecture is designed to run the browser UI and each site in different sandboxes, using processes as the isolation boundary. Firefox and Safari have converged on similar designs. This architecture greatly improves security and reliability at the cost of more memory usage. The web content processes are treated as untrusted, and that’s where site isolation (next section) comes into play to even isolate different origins from each other within separate processes.</p><h3>Site Isolation and Sandboxing</h3><p>Site isolation and sandboxing are security features that build on the multi-process foundation. They aim to ensure that even if malicious code runs in the browser, it cannot easily steal data from other sites or access your system.</p><p><strong>Site Isolation</strong>: We’ve touched on this — it means that different websites (different sites, more strictly) run in different renderer processes. Chrome’s site isolation was boosted after the <a href="https://developer.chrome.com/blog/meltdown-spectre">Spectre vulnerability</a> came to light in 2018. Spectre showed that malicious JavaScript could potentially read memory it shouldn’t (by exploiting CPU speculative execution). If two sites were in the same process, a malicious site could use Spectre to snoop on memory of the sensitive site (like your banking site). The only robust solution is to not let them share a process at all. So Chrome made site isolation a default: every site gets its own process, including cross-origin iframes. Firefox has followed with Project Fission (enabled by default in recent versions), which aims for the same — they cite isolating every site in its own process for security. This is a significant change from the past where if you had a parent page and multiple iframes from various domains, they might all live in one process (especially if they were in one tab). Now, those iframes would be split so that e.g. an &lt;iframe src=”https://evil.com&quot;&gt; on a good site page is forced into a different process, preventing even low-level attacks from leaking info between them.</p><p>From a developer point of view, site isolation is mostly transparent. One implication is that communications between an embedded iframe and its parent might cross process boundaries now, so things like postMessage between them are implemented via IPC under the hood. But the browser makes this seamless; you as a dev just use the APIs as normal.</p><p><strong>Sandboxing</strong>: Each renderer process (and other auxiliary processes) run in a sandbox with restricted privileges. For example, on Windows, Chrome uses a job object and drops privileges so the renderer can’t call most Win32 APIs that access the system. On Linux, it uses namespaces and seccomp filters to limit syscalls. The renderer basically can compute and render content but if it tries to open a file or camera or microphone, it will be blocked (unless going through proper channels that ask user permission via the browser process). WebKit’s documentation explicitly notes that WebContent processes have no direct access to filesystem, clipboard, devices, etc. — they must request via the UI process which mediates. This is why, for example, when a site tries to use your microphone, the permission prompt is shown by the browser UI (browser process) and if allowed, the actual recording is done in a controlled process. The sandbox is a crucial line of defense. Even if an attacker finds a bug to run native code in the renderer, they then face the sandbox barrier — they’d need a separate exploit (an “escape”) to break out to the system. This layered approach (called site isolation + sandbox) is state-of-the-art for browser security.</p><p>Firefox’s sandboxing is also quite strict now (it was weaker in early e10s days but they ramped it up). Firefox content processes can’t directly access much either; and Firefox also sandboxes the GPU process to handle graphics driver issues.</p><p><strong>Out-of-Process iframes (OOPIF)</strong>: In Chrome’s implementation of site isolation, they invented the term <a href="https://www.chromium.org/developers/design-documents/oop-iframes/">OOPIF</a> for out-of-process iframe. From a user’s perspective, nothing changes, but in Chrome’s internal architecture, each frame of a page can potentially be backed by a different renderer process. The top-level frame and same-site frames share one process; cross-site frames use different processes. All those processes “cooperate” to render a single tab’s content, coordinated by the browser process. This is pretty complex, but Chrome has a frame tree that can span processes. It means your one tab might be running N processes (one for the main document, others for each cross-site subdocument). They communicate via IPC for things like DOM events crossing the boundary or certain JavaScript calls that involve cross-context. The web platform (through specs like <a href="https://web.dev/articles/coop-coep">COOP/COEP</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer">SharedArrayBuffer,</a> etc.) is evolving with these constraints in mind after Spectre.</p><p><strong>Memory and Performance Costs</strong>: Site isolation does increase memory usage because more processes are used. Chrome devs noted it could be a 10–20% memory <a href="https://www.thurrott.com/mobile/chrome-os/162980/spectre-mitigation-increases-chrome-memory-usage-google-says">overhead</a> in some cases. They mitigated some by something called “best-effort process consolidation” for same-site, and by limiting how many processes can be spawned (we mentioned earlier). Firefox initially didn’t isolate every site due to memory concerns but after Spectre they found ways to do it more efficiently with 8-privileged-process limit and on-demand process creation. Safari historically has a strong process model but I’m not sure if it isolates cross-site iframes; WebKit2 certainly isolates top-level pages. Apple’s focus is often also on privacy (Intelligent Tracking Prevention will partition cookies, etc.), but that’s a different layer.</p><p>Cross-site prefetches are limited for privacy reasons and will currently only work if the user has no cookies set for the destination site, preventing sites from tracking user activity via prefetched pages that may never be visited.</p><p>All in all, site isolation ensures that the principle of least privilege is applied: code from origin A cannot access data from origin B unless via web APIs with explicit consent (like postMessage or storage that’s partitioned). And the sandbox ensures that even if code is rogue, it can’t touch your system directly. These measures make browser exploits much harder — an attacker typically needs multiple chain exploits now (one to break renderer, one to escape sandbox) to do serious damage, which raises the bar significantly.</p><p>As a web developer, you might not directly feel site isolation, but you benefit from it through a safer web. One thing to be aware of is that cross-origin interactions might have slightly more overhead (because of IPC) and that some optimizations like in-process script sharing aren’t possible across origins. But browsers are continuously optimizing the messaging between processes to minimize any performance hit.</p><p>Now, after covering security, let’s turn to tools and performance instrumentation — essentially, how we developers can peek into this pipeline and measure or debug it.</p><h3>Comparing Chromium, Gecko, and WebKit</h3><p>We’ve mainly described Chrome/Chromium’s behavior (Blink engine for HTML/CSS, V8 for JS, multi-process via Aura/Chromium infrastructure). Other major engines — Mozilla’s Gecko (used in Firefox) and Apple’s WebKit (used in Safari) — share the same fundamental goals and a broadly similar pipeline, but there are noteworthy differences and historical divergences.</p><p><strong>Shared Concepts</strong>: All engines parse HTML into a DOM, parse CSS into style data, compute layout, and paint/composite. All have JS engines with JITs and garbage collection. And all modern ones are multi-process (or at least multi-threaded) for parallelism and security.</p><h3>Differences in CSS/Style System</h3><p>One interesting difference is how CSS style computation is implemented by rendering engine:</p><ul><li><strong>Blink (Chromium)</strong>: Uses a single-threaded style engine in C++ (historically based on WebKit’s). It computes style sequentially for the DOM tree. It has had incremental style invalidation optimizations, but by and large it’s one thread doing the work (apart from some minor parallelization in animation).</li><li><strong>Gecko (Firefox)</strong>: In the Quantum project (2017), Firefox integrated Stylo, a new CSS engine written in Rust, which is multi-threaded. Firefox can calculate style for different DOM subtrees in parallel using all CPU cores. This was a major performance improvement for CSS in Gecko. So, style recalculation in Firefox might use 4 cores to do what Blink does on 1. This is one advantage of Gecko’s approach (at the cost of complexity).</li><li><strong>WebKit (Safari)</strong>: WebKit’s style engine is single-threaded like Blink (since Blink forked from WebKit in 2013, they shared architecture up to that point). WebKit has done interesting things like a bytecode JIT for CSS selectors matching. It may transform CSS selectors into bytecode and JIT compile a matcher for speed. Blink did not adopt that (it uses iterative matching).</li></ul><p>So, in CSS, Gecko stands out with parallel style computation via Rust. Blink and WebKit rely on optimized C++ and maybe some JIT tricks (in WebKit’s case).</p><h3>Layout and Graphics</h3><p>All three engines implement the CSS box model and layout algorithms. Specific features might land in one before others (e.g. at one time WebKit was ahead in CSS Grid support, then Blink caught up — often they share code through standards bodies).</p><p>Firefox (Gecko) made a huge change by introducing <strong>WebRender</strong> as its compositor/rasterizer. WebRender is now the default rendering engine in Firefox and has contributed to significant performance improvements, particularly for graphics-intensive web content. WebRender (also Rust) basically takes the display list and renders it on the GPU directly, handling things like tessellating shapes, text, etc. with the GPU. It’s like moving more painting work to the GPU. In Chrome’s pipeline, rasterization is still done on CPU (for most content) then sent to GPU as bitmaps. WebRender tries to avoid making bitmaps for whole layers and instead draw vectors on GPU (except for text glyphs which it caches as atlas textures). This means Firefox can potentially animate more content at high performance because it doesn’t need to re-rasterize everything if only small portions change — it can redraw via GPU very quickly. It’s akin to how a game engine redraws a scene every frame using GPU calls. The downside is it’s complex to implement and tune, and can stress the GPU more. But as GPU power grows, this approach is forward-looking. Chrome’s team considered a similar approach (“SKIA GPU” path) but has not done a full WebRender style overhaul.</p><p>Safari (WebKit) uses an approach more similar to older Chrome: it has a converts the compositor with layers (called CALayer, since on Mac and iOS it uses Core Animation layers). Safari was early to move to GPU compositing (iPhone OS and Safari 4 in 2009 had hardware-accelerated compositing for certain CSS like transforms). Safari and Chrome diverged but conceptually both do tiling and compositing. Safari also offloads a lot to the GPU (and uses tiling, especially on iOS where tile drawing was fundamental for smooth scrolling).</p><p><strong>Mobile optimizations</strong>: Each engine has special cases for mobile. For example, WebKit has the concept of tile coverage for scrolling (used in iOS’s UIWebView historically). Chrome on Android uses “tiling” and tries to keep raster tasks minimal to hit frame rates. Firefox’s WebRender came from the mobile-first Servo project.</p><h3>JavaScript Engines</h3><ul><li><strong>V8 (Chromium)</strong> we described: Ignition, Sparkplug, TurboFan, Maglev as of 2023.</li><li><strong>SpiderMonkey (Firefox)</strong>: It historically had an interpreter, then a Baseline JIT and an optimizing JIT (IonMonkey). Recent work (Warp) changed how JIT tiers work, potentially simplifying Ion and making it more like TurboFan’s approach to use cached bytecode and type info. SpiderMonkey also has a different GC (also generational, called Incremental GC since 2012, and now mostly incremental/concurrent).</li><li><strong>JavaScriptCore (Safari)</strong>: As noted, it has 4 tiers (LLInt, Baseline, DFG, FTL). It uses a different GC (WebKit’s GC is a generational mark-sweep called Butterfly or Boehm variations historically, now bmalloc etc.). JSC’s FTL uses LLVM to optimize, which is unique (V8 and SM have their own compilers, JSC leverages LLVM for one tier). This can yield very fast code, but the compilation is heavy. JSC tends to prioritize peak performance on certain benchmarks (it often shines on some, but V8 tends to catch up; they leapfrog).</li></ul><p>In terms of ES features, all three engines are pretty much up-to-date with the latest standards, thanks to test262 and each other’s competition.</p><h3>Multi-Process Model Differences</h3><ul><li><strong>Chrome</strong>: each tab typically separate, site isolation at origin level, lots of processes (can be dozens).</li><li><strong>Firefox</strong>: fewer processes by default (8 content processes handling all tabs, plus more if needed for cross-site iframes with Fission). So, it’s not necessarily one process per tab; tabs share content processes in a pool. This means Firefox might have lower memory usage under many-tab scenarios, but it also means one content process crash can take out multiple tabs (though it tries to group by site, so maybe all Facebook tabs in one process, etc.).</li><li><strong>Safari</strong>: likely one process per tab (or per a few tabs) — on iOS, WKWebView definitely isolates each webview. Safari desktop historically did each tab separate as well. Not sure if they isolate cross-origin iframes yet — Apple hasn’t talked about Spectre mitigations much, but Safari does have process per domain for top-level at least.</li></ul><p><strong>Interprocess Coordination</strong>: All engines have to solve similar problems like how to implement alert() (which blocks JS) in a multi-process environment — typically the browser process shows the alert UI and pauses that script context. Or how to handle prompt/confirm, how to do Modal dialogs, etc. There are subtle differences (e.g. Chrome doesn’t truly block the thread for alert — it spins a nested runloop in the renderer, etc. whereas Firefox might still freeze that tab’s process).</p><p><strong>Crash handling</strong>: Chrome and Firefox both have crash reporters that can restart a crashed content process and show an error in the tab. Safari’s Web Content process crash typically will display a simpler error message in the content area.</p><h3>Feature Implementation Divergence</h3><p>Some web platform features are engine-specific: e.g. Chrome has an experimental document.transition API for seamless DOM transitions, which relies on Blink’s architecture. Firefox might implement something differently or later. But eventually, standards converge features.</p><p><strong>Developer tools</strong>: Chrome’s DevTools is very advanced. Firefox’s DevTools also very good (with some unique features like CSS Grid highlighters early on, shape editor). Safari’s Web Inspector is fine but not as full-featured in some areas. These differences can matter to devs debugging in each browser.</p><h3>Performance Trade-offs</h3><p>Historically, Chrome was lauded for faster JS and overall performance due to multi-process and V8. Firefox with Quantum closed a lot of gaps, sometimes surpassing Chrome in graphics (WebRender can be very fast for complex pages). Safari often excels in graphics and low power usage on Apple hardware (they optimize for power a lot).</p><p><strong>Memory</strong>: Chrome has a reputation for high memory usage (all those processes). Firefox tries to be a bit more conservative. Safari is very memory efficient on iOS out of necessity (limited RAM), and they do a lot of memory optimization in WebKit.</p><p><strong>External Contributors</strong>: Interesting note — a lot of improvements in these engines come from external teams like Igalia (e.g. implementing CSS Grid in both WebKit and Blink). So sometimes features land roughly simultaneously.</p><p>From a web developer’s perspective, the differences often manifest as:</p><ul><li>Needing to test on all engines because there might be slight differences or bugs in one’s implementation of a CSS feature or an API.</li><li>Performance might differ (for example, a particular JS workload might be faster in one engine than another due to JIT heuristics).</li><li>Some APIs might not be available in one (Safari is often last to implement some new APIs like WebRTC or IndexedDB versions, etc. though they eventually do).</li></ul><p>But the core concepts we discussed (network -&gt; parse -&gt; layout -&gt; paint -&gt; composite -&gt; JS execution) apply to all, just with varying internal approaches or names:</p><ul><li>In Gecko: parse -&gt; frame tree -&gt; display list -&gt; WebRender scene or layer tree (if WebRender disabled) -&gt; composite.</li><li>In WebKit: parse -&gt; render tree -&gt; graphics layers -&gt; composite (via CoreAnimation).</li></ul><p>And all have analogous subsystems (DOM, styling, layout, graphics, JS engine, networking, processes/threads).</p><p>Knowing these helps in debugging: e.g. if something is janky in Safari but not Chrome, it could be WebKit’s painting differs. Or if CSS is slow in Firefox, maybe it’s hitting a path that isn’t parallelized by Stylo (though that’s rare).</p><p>To sum up, while Chromium, Gecko, and WebKit have different implementations and even some different innovations (parallel CSS in Gecko, WebRender GPU, etc.), they increasingly implement the same web standards and even collaborate on many. The choice of engine matters more for the platform vendors and open web diversity, but as a developer you mostly care that your site runs everywhere. Under the hood, each engine’s unique architecture might lead to different performance profiles or bugs, which is why testing and using performance diagnostics in each (like Firefox’s performance tool vs Chrome’s) can be insightful. It’s beyond our scope to list all differences, but hopefully this gives an idea of the landscape: they are convergent in high-level design (multi-process, similar pipelines) yet divergent in specific technical solutions.</p><h3>Conclusion and Further Reading</h3><p>We’ve journeyed through the life of a web page inside a modern browser — from the moment a URL is entered, through networking and navigation, HTML parsing, styling, layout, painting, and JavaScript execution, all the way to the GPU putting pixels on the screen. We’ve seen that browsers are essentially mini operating systems: managing processes, threads, memory, and a slew of complex subsystems to ensure web content loads fast and runs securely. For web developers, understanding these internals can demystify why certain best practices (like minimizing reflows or using async scripts) matter for performance, or why some security policies (like not mixing origins in iframes) exist.</p><p>A few key takeaways for developers:</p><p><strong>Optimize network usage</strong>: Fewer round trips and smaller files = faster start render. The browser can do a lot (HTTP/2, caching, speculative loading) but you should still leverage techniques like resource hints and efficient caching. The networking stack is high-performance, but latency is always a killer.</p><p><strong>Structure your HTML/CSS for efficiency</strong>: A well-structured DOM and lean CSS (avoid very deep trees or overly complex selectors) can help the parsing and style systems. Understand that CSS and DOM build computed style, then layout computes geometry — heavy DOM manipulations or style changes can trigger these recalculations.</p><p><strong>Batch DOM updates</strong>: to avoid repeated style/layout thrash. Use DevTools Performance panel to catch when your script is causing many layouts or paints.</p><p><strong>Use compositing-friendly CSS for animations</strong>: Animations of transform or opacity stay off the main thread and on the compositor, yielding smooth animations. Avoid animating layout-bound properties if possible.</p><p><strong>Mind the JS execution</strong>: Though JS engines are super-fast, long tasks will block the main thread. Break up long operations (so the page stays responsive) and in some cases consider Web Workers for background tasks. Also, remember that heavy JS can cause GC pauses (rarely long nowadays, but can happen if memory balloons).</p><p><strong>Security features</strong>: Embrace them — e.g. use iframe sandbox or rel=noopener when appropriate, because you now know the browser will isolate those anyway; cooperating with it is good.</p><p><strong>DevTools is your friend</strong>: The performance and network panels in particular are gold mines for seeing exactly what the browser is doing. If something is slow or janky, the tools often point to the cause (a long layout, a slow paint, etc.).</p><p><strong>For those eager to dive even deeper, an excellent resource is Browser Engineering by Pavel Panchekha and Chris Harrelson (available at </strong><a href="http://browser.engineering/"><strong>browser.engineering</strong></a><strong>).</strong></p><p>It’s essentially a free online book that guides you through building a simple web browser, covering networking, HTML/CSS parsing, layout, and more in an accessible way. It can serve as a more in-depth companion to everything we discussed, solidifying knowledge by example. Additionally, the Chrome team’s multi-part series “<a href="https://developer.chrome.com/blog/inside-browser-part1">Inside look at modern web browser</a>” provides a readable overview with diagrams. The V8 blog (<a href="http://v8.dev/">v8.dev</a>) and <a href="https://hacks.mozilla.org/">Mozilla’s Hacks blog</a> are great for learning about engine advances (e.g. new JIT compiler tiers or WebRender internals).</p><p>In conclusion, modern browsers are marvels of software engineering. They successfully abstract away all this complexity so that as developers we mostly just write HTML/CSS/JS and trust the browser to handle it. Yet, by peering under the hood, we gain insights that help us write more performant, robust applications. We appreciate why certain techniques improve user experience (e.g. avoiding blocking the main thread, or reducing unnecessary DOM complexity) because we see how the browser has to work under the covers. The next time you debug a webpage or wonder why Chrome or Firefox behaves a certain way, you’ll have a mental model of the browser’s internals to guide you.</p><p>Happy building, and remember that the web platform’s depth rewards those who explore it — there’s always more to learn, and tools to help you learn it.</p><h3>Further Reading</h3><ul><li><a href="https://browser.engineering/"><strong>Web Browser Engineering</strong></a> — How browsers work deep-dive book</li><li><a href="https://www.youtube.com/playlist?list=PL9ioqAuyl6ULp1f36EEjIN1vSBEfsb-0a"><strong>Chromium University</strong></a><strong> — </strong>Free series of deep-dive videos into how Chromium works, including the excellent <a href="https://www.youtube.com/watch?v=K2QHdgAKP-s&amp;list=PL9ioqAuyl6ULp1f36EEjIN1vSBEfsb-0a&amp;index=3&amp;pp=iAQB">Life of a Pixel talk</a></li><li><a href="https://developer.chrome.com/blog/inside-browser-part1"><strong>Inside the Browser (Chrome Developers Blog series)</strong></a> — parts 1–4 cover architecture, navigation flow, rendering pipeline, and input/controller threads.</li><li><a href="https://addyosmani.com/blog/chrome-17th/"><strong>Google Chrome at 17 — A history of our browser</strong></a></li></ul><p><em>Illustrations in this piece were commissioned from Susie Lu.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7e1cc7337fff" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Chrome now shows each active tab’s memory usage!]]></title>
            <link>https://medium.com/@addyosmani/chrome-now-shows-each-active-tabs-memory-usage-4f74876538e6?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/4f74876538e6</guid>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[web-performance]]></category>
            <category><![CDATA[chrome]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[front-end-development]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Wed, 04 Oct 2023 00:26:37 GMT</pubDate>
            <atom:updated>2023-10-04T00:27:13.031Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YxJ69MRjO3QlU641GrC8KA@2x.jpeg" /></figure><p>In Chrome Canary, we recently introduced <strong>hover cards that show the memory usage for each open tab</strong>. When you hover over a tab, a popup will display the tab’s RAM usage and whether <a href="https://developer.chrome.com/blog/memory-and-energy-saver-mode/">Chrome’s Memory Saver</a> feature has frozen the tab to conserve memory.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cUVa9HAaXAQ3ESXoJ7068g.gif" /><figcaption>Memory usage in hover cards, as tested in Chrome 119+</figcaption></figure><p>This feature provides transparency into Chrome’s tab throttling behaviors and <strong>gives users insight into a site’s memory consumption</strong>.</p><p>The current usage indicates memory consumed by the page itself along with any background processes. State suspension happens when Memory Saver determines the tab has been inactive for a sufficiently long period.</p><p>The new hover cards build on <a href="https://blog.google/products/chrome/new-chrome-features-to-save-battery-and-make-browsing-smoother/">Chrome’s Memory Saver feature</a> and can also be enabled via about:flags:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lRAaEW2Q1cXpylRUbGuFcw.png" /><figcaption>The hover cards feature can be manually enabled using about:flags and enabling the “Show memory usage in hovercards” feature.</figcaption></figure><h3>Memory Saver and Performance Controls</h3><p>Memory Saver puts inactive tabs to “sleep” by freezing them, so their memory can be freed up for active tabs. This helps improve performance when you have lots of tabs open.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fwLeCR60v0-PlE8NCv0X_Q.jpeg" /><figcaption>An example of Memory Saver freeing up memory for other tasks.</figcaption></figure><blockquote>With the latest release of Chrome on desktop, we’re introducing two new performance settings so Chrome uses up to 40% and 10GB less memory to keep your tabs running smoothly, and extend your battery when it’s running low.</blockquote><p>Memory Saver is part of a suite of Performance Controls that Chrome has added recently, including <a href="https://www.zdnet.com/article/how-to-enable-energy-saver-mode-on-google-chrome/">Energy Saver</a> to extend battery life. All of these features aim to intelligently optimize Chrome’s performance and resource usage based on the user’s system constraints.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*py5dx3MhRvwH9-FI3vbIyw.png" /><figcaption>Chrome’s Performance Controls (currently Memory Saver and Energy Saver) give users more control over how the browser utilizes their system resources.</figcaption></figure><p>Chrome uses signals like tab visibility, whether a tab is playing audio, and page lifecycle events to determine if a tab is inactive. When a tab has been in the background for long enough, Chrome will freeze JavaScript execution and place the tab in a low-memory state.</p><p>This helps divert RAM to active foreground tabs and improves performance when you have many tabs open. The tab gets reloaded when it’s brought back into focus.</p><h3>Measuring Memory Usage</h3><p>As web applications grow more complex, memory management has become an increasingly important concern. Memory leaks or inefficient usage can cause performance issues or even crashes. Chrome DevTools provides powerful tools for debugging memory problems — once you know how to use them effectively.</p><h4>Performance Monitor</h4><p>The Performance Monitor gives <strong>high-level, real-time insight </strong>into memory usage with other key metrics like CPU utilization, FPS, and DOM nodes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WJQYBuyMigs92Y5QEJXFjA.png" /><figcaption>The Chrome DevTools Performance monitor. Access it via the three-dot menu.</figcaption></figure><p>Watch for:</p><ul><li>Steadily increasing JS Heap or DOM Nodes over time</li><li>Spikes in JS Heap after certain user actions</li></ul><p>This can indicate a potential memory problem worth investigating further. The Performance Monitor is useful for confirming a suspicion of issues.</p><h4>Measuring Memory Usage with DevTools</h4><p>The Memory panel in Chrome DevTools provides extensive insight into a page’s memory consumption. You can snapshot heap allocation, record allocation timelines, and inspect memory usage by component.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iyi6zhMK4TRLpIHQfL3eDQ.png" /></figure><p>The Summary view shows overall memory usage broken down by category like DOM nodes, JS objects, etc. A comparison of multiple snapshots can detect memory leaks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-Ijl1wwZJRetwrbPmQfBQA.png" /></figure><p>The Allocations Timeline shows real-time allocation activity during interaction. Spikes may indicate inefficient operations. Filtering to a specific component isolates its impact.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jvDcKSpBdKZoSsChTB784w.png" /></figure><p>Finally, the Allocation Sampling view maps memory usage to individual page components like documents, frames, web workers, and graphics layers. This reveals the source of any high usage. This combines the detailed snapshot information of the heap profiler with the incremental updating and tracking of the Performance panel.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IQeSXFVsRKaKAYphe23fTg.png" /></figure><p>Tracking objects’ heap allocation usually involves starting a recording, performing a sequence of actions, then stop the recording for analysis.</p><p>So DevTools provides a powerful set of tools for understanding the memory characteristics of web apps. It’s invaluable for diagnosing leaks and detecting regressions.</p><h4>Using Performance APIs</h4><p>For programmatic memory measurement, the browser API <a href="https://web.dev/monitor-total-page-memory-usage/">performance.measureUserAgentSpecificMemory()</a> allows measuring total memory usage of a page.</p><pre>function scheduleMeasurement() {<br>  // Check measurement API is available.<br>  if (!window.crossOriginIsolated) {<br>    console.log(&#39;performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages&#39;);<br>    console.log(&#39;See https://web.dev/coop-coep/ to learn more&#39;)<br>    return;<br>  }<br>  if (!performance.measureUserAgentSpecificMemory) {<br>    console.log(&#39;performance.measureUserAgentSpecificMemory() is not available in this browser&#39;);<br>    return;<br>  }<br>  const interval = measurementInterval();<br>  console.log(`Running next memory measurement in ${Math.round(interval / 1000)} seconds`);<br>  setTimeout(performMeasurement, interval);<br>}</pre><p>This method returns the aggregated RAM usage of the current page plus all related contexts like iframes and workers. The result includes breakdowns attributing memory usage to specific execution contexts.</p><p>This API can be used to collect real user data on memory consumption. Long-term trends may reveal gradual leaks or regressions after site changes.</p><p>However, measureUserAgentSpecificMemory() reflects each browser’s internal memory representation. Results vary significantly across browsers. Relative comparisons over time are most meaningful.</p><h3>Common Causes of Memory Leaks</h3><p>Many memory leaks in web apps stem from retaining stale references to objects, preventing their cleanup by garbage collection.</p><p>Some common sources include:</p><ul><li>Forgetting to remove event listeners on DOM detachment</li><li>Unintentionally capturing references to DOM elements in closures</li><li>Growing data structures like maps or arrays without clearing</li><li>Failing to close Web Workers properly</li><li>Lingering references between parent and iframe contexts</li><li>Closures and unexpected references are a frequent cause. For example, adding a DOM element inside a closure keeps it referenced even if removed from the page later.</li></ul><p>Memory leaks tend to be introduced inadvertently and often grow gradually over time. But fixing even small leaks can improve performance.</p><p>Profiling memory usage during typical user flows helps identify gradual leak sources. DevTools, <a href="https://github.com/nolanlawson/fuite">fuite</a>, and other tools can pinpoint growing objects. Fixing leaks may require rearchitecting some logic.</p><p>Aim for relatively flat memory usage over time, rather than sawtooth patterns showing gradual accumulation between resets. Leaks under 1MB can still be worth fixing if they add up over long sessions.</p><h3>Conclusion</h3><p>Chrome’s new hover cards provide useful insight into per-tab memory usage for users. For developers, this feature when combined with DevTools, performance APIs, and careful optimization can help you deliver smoother, memory-efficient web apps that users enjoy.</p><h4>Further reading</h4><ul><li><a href="https://developer.chrome.com/blog/memory-and-energy-saver-mode/">What developers need to know about Chrome’s Memory and Energy Saver modes</a></li><li><a href="https://nolanlawson.com/2022/01/05/memory-leaks-the-forgotten-side-of-web-performance/">Memory leaks: the forgotten side of web performance</a></li><li>DevTools: <a href="https://developer.chrome.com/docs/devtools/memory-problems/memory-101/">Memory management documentation</a></li><li>DevTools: <a href="https://developer.chrome.com/blog/memory-inspector/">Introducing the Memory Inspector</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4f74876538e6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Photoshop is now on the web!]]></title>
            <link>https://medium.com/@addyosmani/photoshop-is-now-on-the-web-38d70954365a?source=rss-2508e4c7a8ec------2</link>
            <guid isPermaLink="false">https://medium.com/p/38d70954365a</guid>
            <category><![CDATA[web]]></category>
            <category><![CDATA[case-study]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[chrome]]></category>
            <dc:creator><![CDATA[Addy Osmani]]></dc:creator>
            <pubDate>Sat, 30 Sep 2023 19:48:37 GMT</pubDate>
            <atom:updated>2023-09-30T19:48:37.052Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OPyW9gmaRKNpuyJrZ_-bAw@2x.jpeg" /></figure><p><em>Enabled by WebAssembly + Emscripten, Web Components + Lit, Service Workers + Workbox &amp; new Web APIs. Chrome &amp; Adobe enjoyed collaborating on it.</em></p><p>Bringing the Photoshop desktop application to the web (<a href="https://photoshop.adobe.com/">photoshop.adobe.com</a>) represents an enormous milestone in bringing highly complex and graphically intensive software to the browser. This was made possible through years of effort by Adobe engineers and collaboration with browser vendors like Chrome to push the web forward.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aec6BlOTGEMKmtUCdv8E0Q.png" /><figcaption>Photoshop for web includes a broad range of advanced features, including Generate fill.</figcaption></figure><p>In this case study, we’ll take a close look at the advanced web capabilities unlocked, performance optimizations undertaken, and future possibilities this enables. “<a href="https://web.dev/ps-on-the-web/">Photoshop’s journey to the web</a>” is also an excellent read.</p><h3>The Vision: Photoshop in the Browser</h3><p>For decades, Photoshop has been the gold standard in image editing and graphic design, powering creativity across Windows and macOS. But a world of new opportunities opens up by untethering it from the desktop.</p><p><strong>The web promises ubiquitous, frictionless access.</strong> Users can start editing and collaborating instantly with just a browser, no installation needed. And they can pick up seamlessly across devices.</p><p><strong>Linkability enables sharing workflows.</strong> Photoshop documents can be accessed at a URL, rather than buried in the files system. Creators can easily send a link to collaborators.</p><p><strong>Cross-platform flexibility.</strong> The web as a runtime abstracts away underlying operating systems. Photoshop can reach users on multiple platforms.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OvEk3CD7LdwCpR-zHZIUZQ.png" /></figure><p>But substantial technical challenges stood in the way of realizing this vision, requiring rethinking how an intense app like Photoshop could work on the web.</p><h3>New Web Capabilities Unlock Photoshop’s Potential</h3><p>In recent years, new web platform capabilities have emerged that can finally enable Photoshop-class applications, through both standardization and implementation. Adobe engineers made innovative use of several key next-generation APIs:</p><h3>High-Performance Local File Access with the Origin Private File System</h3><p>Photoshop operations involve reading and writing potentially massive PSD files. This requires efficient access to a local file system. The new <a href="https://web.dev/origin-private-file-system/">Origin Private File System API</a> (OPFS) provides a fast, origin-specific virtual file system.</p><pre>const opfsRoot = await navigator.storage.getDirectory();</pre><p>OPFS enables quickly creating, reading, writing, and deleting files. For example:</p><pre>// Create file<br>const file = await opfsRoot.getFileHandle(&#39;image.psd&#39;, {create: true}); <br><br>// Get read/write handle <br>const handle = await file.createSyncAccessHandle();<br><br>// Write contents  <br>handle.write(buffer); <br><br>// Read contents<br>handle.read(buffer);<br><br>// Delete file<br>await file.remove();</pre><p>For the absolute fastest synchronous operations, Web Workers can get a FileSystemSyncAccessHandle.</p><p>This local high-performance file system was critical for implementing Photoshop’s demanding file workflows in the browser.</p><h3>Unleashing the Power of WebAssembly</h3><p>WebAssembly was an essential ingredient for recreating Photoshop’s computationally intensive graphics processing in JavaScript. Adobe used the <a href="https://emscripten.org/">Emscripten</a> compiler to port their existing C/C++ codebase to WebAssembly modules.</p><p>Several WebAssembly capabilities were critical:</p><ul><li><strong>Threads</strong> — Photoshop uses worker threads for parallel execution of tasks like processing image tiles:</li></ul><pre>// Thread function<br>void* tileProcessor(void* data) {<br>  // Process image tile data <br>  return NULL;<br>}<br><br>// Start worker threads <br>pthread_t thread1, thread2;<br>pthread_create(&amp;thread1, NULL, tileProcessor, NULL);<br>pthread_create(&amp;thread2, NULL, tileProcessor, NULL);<br><br>// Wait for threads to finish<br>pthread_join(thread1, NULL);<br>pthread_join(thread2, NULL);</pre><ul><li><strong>SIMD</strong> — SIMD vector instructions accelerate pixel manipulations and filtering.</li><li><strong>Exception handling</strong> — C++ exceptions are widely used throughout Photoshop’s codebase.</li><li><strong>Streaming instantiation</strong> — Photoshop’s 80MB+ WASM modules require streaming compilation.</li><li><strong>Debugging</strong> — Chrome’s WebAssembly debugging support in DevTools was invaluable.</li></ul><h3>Harnessing the Wide P3 Color Gamut</h3><p>sRGB’s color spectrum pales in comparison to the wider P3 gamut, but it has long been the only option on the web.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xuy7do_Lm9sO-3KPHjLuJw.png" /></figure><p>Photoshop uses the new <a href="https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/">color() function and Canvas APIs</a> to unlock the full vibrancy of P3, enabling more accurate color representation.</p><pre>color: color(display-p3 1 0.5 0)</pre><h3>UI Flexibility with Web Components</h3><p>Photoshop is part of Adobe’s broader Creative Cloud ecosystem. Using a standardized <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components">Web Components</a> strategy built on <a href="https://lit.dev/">Lit</a> allows UI consistency across applications.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6zRhpCq7ewr66PNG1Q1nIA.png" /></figure><p>Photoshop’s UI elements come from Adobe’s <a href="https://opensource.adobe.com/spectrum-web-components/">Spectrum Web Components</a> library, which implements Adobe’s design system.</p><p>Spectrum Web Components are:</p><ul><li><strong>Accessible by default</strong> — Developed with existing and emerging browser specs in mind to support assistive technologies.</li><li><strong>Lightweight</strong> — Implemented with LitElement for minimal overhead.</li><li><strong>Standards based</strong> — Built on Web Component standards like custom elements and Shadow DOM.</li><li><strong>Framework agnostic</strong> — Work with any framework thanks to browser-level support.</li></ul><p>Furthermore, the entire Photoshop app is built using Lit-based Web Components. Lit’s templating and virtual DOM diffing allow efficient UI updates. Web Components encapsulation also made it easy to integrate React code from other teams when needed.</p><p>Overall, Web Components’ browser-native custom elements, combined with Lit’s performance, provided the flexibility Adobe needed to build Photoshop’s complex UI while maintaining efficiency.</p><h3>Optimizing Photoshop’s Performance in the Browser</h3><p>While new web capabilities provided the foundations, an intensive desktop application like Photoshop still needed extensive tracing and performance work to deliver a first-class experience online.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0jQ-FEeS24J-oBn20KwSvQ.png" /><figcaption>Long tasks broken up in the loading of the initial Photoshop web application</figcaption></figure><h3>Caching Assets and Code with Service Workers</h3><p><a href="https://web.dev/learn/pwa/service-workers/">Service Workers</a> allow web apps to cache their assets, code, and other resources locally for much faster load times after the initial visit. Although not yet a fully offline-capable app, Photoshop already leverages Service Workers to cache its WebAssembly modules, scripts, and other assets.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3rt282XMzu40-jJGbNVvPA.png" /><figcaption>Chrome DevTools Application panel &gt; Cache storage showing the different kinds of resources being precached by Photoshop for web. Here ew see a number of the JavaScript chunks that they code-split that are locally cached, enabling very fast subsequent loads.</figcaption></figure><p>This caching makes a dramatic difference in load performance. After the first visit, subsequent loads are typically very fast (M1 Macbook):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5hB0UHU99XO5Ip2CrlcP6g.gif" /></figure><p>Adobe used the <a href="https://developer.chrome.com/docs/workbox/">Workbox</a> libraries to more easily integrate Service Worker caching into their build process.</p><p><strong>V8 Optimization of Cached Resources</strong></p><p>V8 employs some optimizations when resources are returned from Service Worker caches:</p><ul><li>Resources cached during install are compiled eagerly and code cached immediately for consistent, fast performance.</li><li>Resources cached via the Cache API during fetch get optimized caching on the 2nd load, faster than typical caching.</li><li>V8 detects resource importance based on being cached and compiles more aggressively.</li></ul><p>These allow optimizing Photoshop’s massive cached Wasm modules.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c3ATOT6wozL6D3AIJYC6dw.png" /><figcaption>A subsequent load of Photoshop web loading key views faster thanks in part to V8 code-caching.</figcaption></figure><h3>Streaming and Caching Large WebAssembly Modules</h3><p>Photoshop’s codebase requires multiple large WebAssembly modules, some over 80MB. Streaming compilation support in V8 and Chrome allows these massive modules to be handled performantly.</p><p>In addition, the first time a WebAssembly module is requested from a Service Worker, V8 generates and stores an optimized version to cache — critical for Photoshop’s large code size.</p><h3>Multithreading for Parallel Graphics Operations</h3><p>Many core image processing operations in Photoshop like transforming pixels can be massively sped up through parallel execution across threads. WebAssembly’s <a href="https://webassembly.org/docs/threads/">threads support</a> unlocks leveraging multi-core devices for computationally intensive graphics tasks.</p><p>This allows Photoshop to use the same multithreading approaches from desktop for performance-critical image processing functions ported to WebAssembly.</p><h3>Debugging WebAssembly for Optimization</h3><p>Robust <a href="https://developer.chrome.com/blog/wasm-debugging-2020/">WebAssembly debugging support</a> was critical for diagnosing and fixing performance bottlenecks during development.</p><p>Chrome DevTool’s ability to profile WASM code, set breakpoints, and inspect rich variables mirrors the debuggability of JavaScript:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BuMXUug1DTbRkJHN" /></figure><h3>Integrating On-Device Machine Learning with TensorFlow.js</h3><p>Recent versions of Photoshop on the web include AI-powered capabilities using <a href="https://www.tensorflow.org/js">TensorFlow.js</a>. Running models on-device rather than in the cloud improves privacy, latency, and cost.</p><blockquote>“TensorFlow.js is an open-source machine learning library from Google aimed at JavaScript developers that’s able to run client side in the browser. It’s the most mature option for web ML with comprehensive WebGL and WebAssembly backend operators support, and in the future, there will also be an option for a WebGPU backend to be used within the browser for faster performance as new web standards evolve.”</blockquote><p>The <strong>Select Subject</strong> feature uses machine learning to automatically extract the main foreground object in an image, drastically speeding up complex selections.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*03Q4MYAOm4xU1z-b_nMi-Q.png" /><figcaption>I have an illustration of a sunset that I wanted to change to be set at night. I used “select subject” and an AI prompt to try picking the region(s) of greatest interest to update.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*stKQcyt8h8G7gMlBa1b7nw.png" /><figcaption>Photoshop was able to both generate an updated illustration based on my AI prompt</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jkFtItbtqyAHqbyxfWrb1Q.png" /><figcaption>…as well as several beautiful variations.</figcaption></figure><p>The model was converted from TensorFlow to TensorFlow.js to enable local execution:</p><pre>// Load Select Subject model<br>const model = await tf.loadGraphModel(&#39;select_subject.json&#39;);<br><br>// Run inference on image tensor  <br>const {mask, background} = model.execute(imgTensor);<br><br>// Refine selection from mask</pre><p>Adobe and Google collaborated to resolve synchronization issues between Photoshop’s WebAssembly code and TensorFlow.js by developing a proxying API for Emscripten. This allows seamless integration between the frameworks.</p><blockquote>“Since the Google team has improved TensorFlow.js hardware execution performance via its various supported backends (WebGL, WASM, Web GPU), it has resulted in <strong>models seeing anywhere from 30% to 200% performance improvements </strong>(especially for the larger models that tend to see the biggest gains), enabling close to real time performance right in the browser.”</blockquote><p>Key models were optimized focusing on performance-critical operations like Conv2D. Photoshop can choose whether to run models on-device or in the cloud based on performance needs.</p><p>See the <a href="https://blog.tensorflow.org/2023/03/how-adobe-used-web-ml-with-tensorflowjs-to-enhance-photoshop-for-web.html">TensorFlow.js Photoshop article</a> for more details.</p><h3>The Future of Photoshop on the Web</h3><p>The general availability of Photoshop on the web represents an enormous milestone, but only scratches the surface of the possibilities.</p><p>Photoshop will continue to expand on the web, with more features coming online through progressive enhancement as browser vendors evolve standards and performance. And Photoshop is just the beginning. Adobe plans to aggressively build out its entire Creative Cloud suite on the web, unlocking many more sophisticated design applications in the browser.</p><p>The collaboration between Adobe and browser engineers will continue driving the web forward as a platform for ever more ambitious applications through advancing standards and performance improvements. There are exciting times ahead!</p><h3>Trying Photoshop on the Web</h3><p>Photoshop on the web is currently available on desktop versions of:</p><ul><li>Chrome 102+</li><li>Edge 102+</li><li>Firefox 111+</li></ul><p>and work to close the gap in support for Safari is underway.</p><p>You can try it out today at <a href="https://photoshop.adobe.com/">photoshop.adobe.com</a>. The future of creativity and design on the web is right at your fingertips!</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FCF5zZZy0R9U%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DCF5zZZy0R9U&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FCF5zZZy0R9U%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/e055d5e388de897c90c1cebd4b5a98c3/href">https://medium.com/media/e055d5e388de897c90c1cebd4b5a98c3/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=38d70954365a" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>