---
id: wiki-2026-0508-soft-navigation
title: Soft Navigation
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [SPA Navigation, Client-side Routing, Soft Nav]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [web-performance, web-vitals, spa, navigation, history-api]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
language: JavaScript
framework: React/Next.js/Remix
---
# Soft Navigation
## 매 한 줄
> **"매 page 의 reload 없이 매 URL + DOM 의 swap"**. Soft navigation 매 SPA pattern, History API (`pushState`) + view transitions 매 modern impl. 매 2025+ 의 Web Vitals 매 soft-nav LCP/INP 의 attribute (Soft Navigations API), 매 Chrome 138+ stable.
## 매 핵심
### 매 hard vs soft 의 line
- **Hard nav**: 매 full document load, 매 `unload`/`load`, fresh JS context.
- **Soft nav**: 매 client-side router. URL 매 `history.pushState`, DOM 매 partial swap. 매 same JS context.
- **CWV impact**: 매 v1 vitals 매 hard 만 measure. 매 2025 Soft Navigations API 매 soft 도 LCP/INP/CLS 의 attribute.
### 매 detection criteria (Chrome)
1. User-initiated (click, keypress).
2. URL change via History API.
3. DOM change (significant) post-event.
4. 매 셋 의 모두 conjunction → soft-nav event.
### 매 view transitions API
- **same-doc**: `document.startViewTransition(() => updateDOM())`. 매 2024 stable.
- **cross-doc** (MPA mode): 매 2025 Chrome 126+. `@view-transition { navigation: auto }`.
- 매 Web Animations API 의 wrap, 매 CSS `::view-transition-*` 의 customize.
### 매 응용
1. Next.js App Router (RSC streaming + soft nav).
2. Remix nested routes.
3. Astro view-transitions.
4. SvelteKit goto().
## 💻 패턴
### Native History API
```javascript
// router.js
class SoftRouter {
constructor() {
window.addEventListener('popstate', this.render.bind(this));
document.addEventListener('click', this.intercept.bind(this));
}
intercept(e) {
const a = e.target.closest('a[data-soft]');
if (!a) return;
e.preventDefault();
this.navigate(a.href);
}
async navigate(url) {
history.pushState({}, '', url);
await this.render();
}
async render() {
const path = location.pathname;
if (document.startViewTransition) {
document.startViewTransition(() => this.update(path));
} else {
this.update(path);
}
}
update(path) {
document.querySelector('main').innerHTML = renderRoute(path);
}
}
new SoftRouter();
```
### Next.js 15 App Router
```tsx
// app/products/[id]/page.tsx
import { Suspense } from 'react';
export default async function Product({ params }) {
const data = await fetch(`/api/p/${params.id}`).then(r => r.json());
return (