We've shipped four App Router migrations in the last year. Here's what we'd tell the next team starting.
Server Components are a layout choice, not a styling choice
The mental shift that matters: which components hold state, and which compose data. Don't try to convert page by page. Identify the data-composition boundary in your product and let that be your migration unit.
// data composition lives here
async function Dashboard() {
const widgets = await getWidgets();
return (
<Shell>
{widgets.map(w => <Widget key={w.id} data={w} />)}
<ClientSidebar /> {/* state lives here */}
</Shell>
);
}Caching is the product
Most App Router pain we've seen comes from misunderstanding the fetch cache. Read the docs once, then write a one-page cheat sheet your team agrees on. We pin ours in the repo's README.
Don't fight Suspense
If a section of your page is slow, wrap it in Suspense with a skeleton and let the rest render. This is the single biggest UX win we get from the migration.
