How Astro Works
What this module is for
Section titled “What this module is for”Every earlier module in this course taught you to use Astro: components, islands, content collections, actions, middleware, adapters. None of it required knowing what actually happens to a .astro file after you write it, what ships to the browser when you add client:load, what astro sync actually writes to disk, what order a request runs through, or what astro build does between “Collecting build info…” and “Complete!”. You can write a working component, a correct loader, a passing action — without ever asking what $$createComponent is, why an island waits for its parent to hydrate first, or why an unchanged content entry doesn’t get reparsed.
That gap is exactly where AI-generated Astro fails in ways that look fine on the surface: a nested island that assumes it hydrates independently of its parent, a custom loader that mutates the data store without a digest so every build reparses everything, a middleware that reads context.request.body twice, a route collision “fixed” by renaming a file instead of understanding static/dynamic/rest priority. None of these are syntax errors. None of them are caught by astro check. All of them are facts about the runtime and the build.
This module has one job: give you five pieces of runtime knowledge that let you look at someone else’s Astro code — not run it, not profile it, just read it — and answer:
- What does this
.astrofile actually compile to? (the compiler output —$$createComponent,$$render, scoped styles, slots, hydration metadata) - What happens the instant an island becomes interactive — and in what order, when islands are nested? (island hydration — the real
<astro-island>element, prop encoding, directive loaders) - What does
astro syncwrite to disk, and when does a loader actually re-run? (the content layer — the data store, digests, live vs. build-time loaders) - What order does an incoming request actually run through? (the request pipeline — adapter → middleware → routing → page/endpoint/action → response)
- What does
astro builddo, in what order, and why is a second build often faster? (the build pipeline — Vite 8 + Rolldown, the prerender pass, the client bundle, the content-layer cache)
Every claim in this module is backed by one of three things: docs.astro.build, the actual Astro source shipped in node_modules/astro (this course pins 7.3.5), cited by file and function name; or a real, reproduced run — a transform() call against the installed @astrojs/compiler-rs, a vitest assertion, a real curl against a running build, or a real astro build/astro sync log — pasted below the claim, never a guess about internals.
What this module covers
Section titled “What this module covers”| Lesson | Answers the question |
|---|---|
| Compiler Output | What does a .astro file with props, a scoped style, an expression, and a slot actually compile to? |
| Island Hydration | What does <astro-island> actually do at connectedCallback, and why do nested islands hydrate in a specific order? |
| Content Layer Internals | What does astro sync write to disk, and how does a loader know an entry hasn’t changed? |
| Request Pipeline | What is the real, provable order a request runs through, from the adapter to the response? |
| Build Pipeline | What does astro build actually do, in what order, and what makes a second build faster? |
The Foundations module already introduced “Astro ships zero JavaScript by default” and the component model from the outside. This module goes past that framing into the actual compiled output, the actual runtime, and the actual build.
A runnable question: what does a scoped style actually become?
Section titled “A runnable question: what does a scoped style actually become?”Before any of the deeper mechanism, here is a fact this whole module orbits: compiling the exact same .astro source twice, under two different file names, produces two different scoped-style hashes — proving the hash is derived from the file’s path, not from a counter or the content alone.
import { transform } from '@astrojs/compiler-rs';import { expect, test } from 'vitest';
const source = `---const { name } = Astro.props;---<style> .card { color: red; }</style><div class="card">Hello {name}</div>`;
test('transform(): the scoped-style hash is derived from the filename, not the content', () => { const a = transform(source, { filename: 'src/components/Card.astro' }); const b = transform(source, { filename: 'src/components/RenamedCard.astro' });
const hashOf = (code: string) => code.match(/astro-([a-z0-9]+)/)?.[1];
expect(hashOf(a.code)).toBeTruthy(); expect(hashOf(b.code)).toBeTruthy(); expect(hashOf(a.code)).not.toBe(hashOf(b.code)); // same source, different file -> different hash});✓ transform(): the scoped-style hash is derived from the filename, not the content
Test Files 1 passed (1) Tests 1 passed (1)Compiler Output walks the rest of this same transform() call in full — every import, every $$-prefixed call, and exactly why the hash changes with the filename.
flowchart TD src["`.astro` source"] --> compile["`@astrojs/compiler-rs` `transform()`"] compile --> js["Compiled JS: `$$createComponent`/`$$render` `$$addAttribute`/scoped `astro-*` hash (Compiler Output)"] js --> island["If hydrated: `<astro-island>` ships connectedCallback -> hydrate() (Island Hydration)"] content["`astro sync`: content collections -> data store digest-keyed, skips unchanged entries"] --> js req["Incoming Request"] --> adapter["adapter entry (e.g. `@astrojs/node`)"] adapter --> mw["middleware `sequence()`"] mw --> route["route matching: static > dynamic > rest (Request Pipeline)"] route --> js js --> resp["Response"] build["`astro build`: Vite 8 + Rolldown prerender pass -> client bundle (Build Pipeline)"] --> js
Mental Model
Section titled “Mental Model”Reading unfamiliar Astro well means running a small machine in your head while your eyes only see the file: what does this compile to, does it ship any JavaScript, and if it does, when does that JavaScript actually run relative to everything else on the page and in the request. The five lessons here are exactly those questions, at compile time (Compiler Output), at hydration time (Island Hydration), at sync/build time for content (Content Layer Internals), per-request (Request Pipeline), and for the whole site (Build Pipeline). Nothing here is exotic — the compiled output is plain, readable JavaScript, and the runtime is a few hundred lines you can read directly in node_modules/astro/dist/.
Common Misconceptions
Section titled “Common Misconceptions”- “Astro’s compiler is
@astrojs/compiler, the WASM package.” Proven wrong in this module: Astro 7’s real compiler — the oneastro check/astro buildactually run — is@astrojs/compiler-rs, a native Rust NAPI addon.@astrojs/compiler(the older, browser-loadable WASM package) still exists and is what this course’s own in-browser<AstroPlayground>uses, because the real Rust compiler has no browser build — but it is not the compiler that gates this course’s own snippets. - “An island hydrates as soon as its own directive fires, independent of anything else on the page.” Island Hydration proves the opposite with the real, installed
astro-island.js: a nested island whose parent is still server-rendered-only (ssrattribute present) defers its own hydration until the parent actually hydrates. - “Every
astro buildreparses every content entry from scratch.” Content Layer Internals proves, with a real, unmodifiedglob()loader and two consecutiveastro syncruns, that an entry whose content digest hasn’t changed is never reparsed — only its module/asset imports are re-registered.
Reading AI-generated code
Section titled “Reading AI-generated code”The whole point of this module is this section, five times over — one per lesson. As a preview, here is the survey-level version: a snippet that renders correctly the first time and is still wrong.
🐛 Spot the bug
---import Inner from './Inner.jsx';---<div class="panel"><Inner client:load /></div>
<script>// "runs once the island is interactive"document.querySelector('.panel')?.classList.add('ready');</script>Show the answer
This looks reasonable and even “works” — the class gets added. But the comment’s claim is wrong: a plain <script> on the page runs on DOMContentLoaded/module-execution order, which has no relationship at all to when Inner’s island actually finishes hydrating. Island Hydration proves the real signal: astro-island.js dispatches a real astro:hydrate CustomEvent on the island element itself, only once hydration has actually completed (this.dispatchEvent(new CustomEvent('astro:hydrate')) in the installed source). The fix is to listen for that event on the island’s <astro-island> element (or a stable ancestor) instead of assuming script-tag order implies hydration order — document.querySelector('astro-island')?.addEventListener('astro:hydrate', () => { ... }).