
SPA vs MPA: The War on Refresh
Why modern web feels like an App (SPA) vs Old School (MPA). And the Hybrid Solution (Next.js).

Why modern web feels like an App (SPA) vs Old School (MPA). And the Hybrid Solution (Next.js).
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Back in 2010, reading Naver News always went like this:
Click article → White flash → Loading → New page
I was frustrated. "Why does it redraw the screen every time?"
But in 2024, browsing Facebook or Twitter:
Click post → Smooth transition → No flicker
This was fascinating to me. Both are websites, but one feels like flipping through a book (slow), while the other feels like using an app (fast). I wanted to know what made them different.
It happened when I built my first project with React.
Development: Perfect ✅
Deployment: Perfect ✅
Google Search: Not indexed ❌
My site wasn't showing up on Google at all. And it took 5 seconds for the first screen to load. When users hit the back button, weird things happened.
A senior developer told me:
"You don't understand the difference between SPA and MPA. Migrate to Next.js."
Honestly, I had no idea what they meant. SPA? MPA? Single Page Application? What's that?
That's when I started studying web rendering patterns, treating it as personal learning notes for myself.
When I first read about SPA, it didn't click at all.
"SPA is a way to implement multiple pages using a single HTML file."
I read this sentence 10 times and still didn't get it. How can one HTML file become multiple pages? Then how does the URL change?
And then there were all these acronyms: "CSR", "SSR", "SSG". I was completely lost.
The most frustrating part was:
"How exactly is Facebook implemented?"
I just wanted to know that one thing.
A senior developer gave me an analogy. That moment, everything clicked in my head.
Think of reading a book.
Going from Chapter 1 → Chapter 2:
- You completely close the book
- You reopen it
- The cover (header) shows again
- The table of contents (navigation) reloads
→ Feels like opening a "completely new book" every time
Think of flipping through presentation slides.
Going from Slide 1 → Slide 2:
- The top header stays
- Only the content changes
- Smooth transition effects
- But you need to download the entire slide file first
→ Feels like "changing screens within one file"
This analogy resonated with me. "Oh, it's about whether you replace the entire page (MPA) vs just the content (SPA)!"
I finally accepted what it really was. The reason Facebook is fast is because it doesn't fetch new HTML from the server—the browser just changes the screen using JavaScript.
This is the traditional website approach. WordPress and Naver Blog work this way.
User: Clicks "Home"
1. Browser: "Server, give me home.html"
2. Server: "Here you go" (sends HTML)
3. Browser: Redraws entire screen (flicker)
User: Clicks "Blog"
4. Browser: "Server, give me blog.html"
5. Server: "Here you go" (sends HTML)
6. Browser: Redraws entire screen (flicker again)
→ Refresh every time (white screen flash)
<!-- home.html -->
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<h1>Welcome!</h1>
</main>
</body>
</html>
<!-- blog.html -->
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
<link rel="stylesheet" href="style.css"> <!-- ← Downloaded again! -->
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</nav> <!-- ← Header redrawn again! -->
</header>
<main>
<h1>Blog Posts</h1>
</main>
</body>
</html>
The files are completely separate. Each page has its own HTML. The browser must fetch new HTML from the server every time.
1. SEO Champion:
- Google bot reads HTML directly
- Good search rankings
- Easy crawling
2. Simple Implementation:
- Just create HTML files
- Almost no JavaScript needed
- Only need backend like PHP/Django
3. Fast Initial Load:
- Only download the page you need
- No unnecessary code
These drawbacks really hit home for me. This was exactly the frustration I felt back in 2010.
1. Flicker:
- Screen refresh on every page transition
- Poor UX
- Doesn't feel modern
2. Duplicate Transfer:
- Header, footer re-downloaded every time
- CSS, JavaScript re-requested (except caching)
- Network waste
3. Slow:
- Server round-trip every time (RTT)
- Server HTML generation time
- Users keep seeing white screens
This is where I finally felt like I understood it.
User: First visit
1. Browser: "Server, give me all the code"
2. Server: "Here you go" (index.html + huge bundle.js)
3. Browser: Execute JavaScript
4. React/Vue app starts
User: Clicks "Home" → "Blog"
1. JavaScript: Change URL (history.pushState)
2. JavaScript: Update screen content only
3. No server request!
4. No flicker!
User: Needs data
1. JavaScript: "Server, just give me JSON" (fetch/axios)
2. Server: "{ posts: [...] }"
3. JavaScript: Update screen with JSON data
The key is "download all JavaScript initially, then the browser handles screen changes on its own."
// App.jsx (React)
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog" element={<Blog />} />
<Route path="/about" element={<About />} />
</Routes>
</Layout>
</BrowserRouter>
);
}
function Layout({ children }) {
return (
<>
<header>
<nav>
<Link to="/">Home</Link> {/* ← No refresh on click! */}
<Link to="/blog">Blog</Link>
<Link to="/about">About</Link>
</nav>
</header>
<main>
{children} {/* ← Only this part changes */}
</main>
</>
);
}
function Blog() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// Fetch only data via AJAX
fetch('/api/posts')
.then(res => res.json())
.then(setPosts);
}, []);
return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.summary}</p>
</article>
))}
</div>
);
}
Notice there's only one HTML file (index.html). JavaScript changes the pages inside it. When you click <Link>, React Router just changes the URL and updates the screen.
1. UX Champion:
- No flicker
- App-like smoothness
- Page transition animations possible
- This is why Facebook and Twitter feel fast
2. Fast Page Transitions:
- Change screen without server requests
- Only fetch JSON data (smaller than HTML)
- Instant response
3. State Persistence:
- JavaScript variables persist across page changes
- Music can play during page transitions
- YouTube videos keep playing
This is why I struggled so much.
1. Slow Initial Load:
- Download all JavaScript (bundle.js is MBs)
- First visit is slow
- Users see "blank screen" for a while
2. SEO Problems:
- Google bot sees empty HTML
- <div id="root"></div> ← No content!
- Content appears only after JavaScript execution
- Google can execute JavaScript but it's slow
3. JavaScript Required:
- Blank screen if JS is disabled
- Entire app crashes if JavaScript errors occur
This is a real problem I faced.
Built a blog with React:
- Dev environment: Perfect ✅
- Production deployment: Perfect ✅
- Google search: Not indexed after 3 weeks ❌
Reason:
What Google bot saw:
<html>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
→ No content at all!
→ Google: "This site is empty? Won't index it."
My blog had 20 articles, but from Google's perspective, it was a completely empty site. This was so frustrating.
Migrated to Next.js:
- Server generates HTML beforehand (SSR/SSG)
- Google bot sees complete HTML
- Indexed on Google in 3 days
- Search rankings improved too
That's when I accepted it. "SPA has great UX but poor SEO. To get both, you need a framework like Next.js."
This was the real core. The moment I understood this concept, everything connected.
"First page like MPA, subsequent navigation like SPA"
1. First Visit:
- Server sends complete HTML (MPA style)
- Google bot: "Oh, there's HTML!" ✅
- User: "The screen loaded fast!" ✅
2. Subsequent Page Navigation:
- JavaScript changes only the screen (SPA style)
- No flicker ✅
- Smooth ✅
→ Takes both MPA advantages (SEO, fast first load) + SPA advantages (smoothness)!
I thought this approach was really clever. "Why didn't they make this earlier?" But it turns out it was technically complex, so Next.js only came out in 2016.
// pages/index.js (Home page)
export default function Home({ posts }) {
return (
<div>
<h1>Latest Posts</h1>
{posts.map(post => (
<Link key={post.id} href={`/post/${post.slug}`}>
<article>
<h2>{post.title}</h2>
<p>{post.summary}</p>
</article>
</Link>
))}
</div>
);
}
// This function runs only on the server (SSR)
export async function getServerSideProps() {
// Fetch posts from database
const posts = await db.getPosts();
return {
props: { posts } // ← This data is included in HTML
};
}
When a user visits the / page:
getServerSidePropsposts from DBWhat Google bot sees:
<div>
<h1>Latest Posts</h1>
<article>
<h2>First Post Title</h2>
<p>Summary...</p>
</article>
<article>
<h2>Second Post Title</h2>
<p>Summary...</p>
</article>
</div>
All the content is there! Perfect SEO!
This is Next.js's magical concept. I didn't understand it at first, but after hearing an analogy, it clicked.
1. Server: Generate HTML (skeleton)
<div>
<h1>Latest Posts</h1>
<button>Like</button> ← Not clickable (static HTML)
</div>
2. Browser: Display HTML immediately (fast!)
- User already sees the screen
- But button doesn't work yet
3. JavaScript downloads & executes
4. React "breathes life" into HTML (Hydration)
- Attach event listeners to button
- Now clickable!
5. Click links afterward:
→ Transition without page refresh (SPA style)
The word "Hydration" made sense to me. It's like pouring water (JavaScript) into a dry sponge (static HTML) to make it a living app.
Let me organize these three concepts:
Traditional SPA method (Create React App):
1. Browser receives empty HTML + bundle.js
2. JavaScript executes
3. Screen renders
Pros: No server load
Cons: Slow first load, weak SEO
Examples: Admin dashboards, internal tools
Next.js dynamic method:
1. Server generates HTML on every request
2. Browser receives complete HTML
3. JavaScript loads → Hydration
Pros: Fast first load, good SEO
Cons: High server load
Examples: News sites, SNS timelines
Next.js code:
export async function getServerSideProps() { ... }
Generated at build time:
1. Create HTML during build
2. Upload to CDN
3. Send HTML instantly on request (super fast!)
Pros: Fastest, no server load, perfect SEO
Cons: Need rebuild when data changes
Examples: Blogs, documentation, marketing pages
Next.js code:
export async function getStaticProps() { ... }
| Method | First Load Speed | SEO | Server Load | Real-time Data | Best For |
|---|---|---|---|---|---|
| CSR | Slow (3-5s) | Weak ❌ | None | Possible ✅ | Dashboards, admin panels |
| SSR | Fast (0.5-1s) | Strong ✅ | High ⚠️ | Possible ✅ | News, SNS, e-commerce |
| SSG | Ultra-fast (0.1s) | Strong ✅ | None ✅ | Not possible ❌ | Blogs, docs, portfolios |
This is what I actually applied to my projects.
// 1. Blog posts: SSG (content rarely changes)
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return {
props: { post },
revalidate: 3600 // Regenerate every hour (ISR)
};
}
// 2. Real-time stock chart: SSR (changes frequently)
export async function getServerSideProps() {
const stock = await fetchStockPrice();
return {
props: { stock }
};
}
// 3. User dashboard: CSR (private data, SEO unnecessary)
export default function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/user/dashboard')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <Loading />;
return <div>{data.name}'s Dashboard</div>;
}
Understanding this strategy solves 99% of web projects.
These are actual numbers I measured when migrating my blog.
First load time: 4.2s
Google search: Not indexed after 3 weeks
Lighthouse score: 42
FCP (First Contentful Paint): 3.8s
TTI (Time to Interactive): 5.1s
User feedback: "It's slow"
First load time: 0.8s (↓ 80%)
Google search: Top results in 3 days
Lighthouse score: 98
FCP: 0.4s
TTI: 1.2s
User feedback: "Much faster!"
After seeing these numbers, I was convinced. "Next.js isn't optional—it's essential."
Got excited learning Next.js:
- Blog posts: SSR
- About page: SSR
- Contact page: SSR
Result:
- Server CPU at 100%
- Actually slower
- Server costs exploded
Lesson learned:
→ Use SSG for static pages
→ Use SSR only for truly dynamic content
// Rendered on server
<div>Current time: {new Date().toString()}</div>
// Hydrated on client
<div>Current time: {new Date().toString()}</div>
→ Time is different, error!
Warning: Text content did not match.
Server: "2024-02-04 17:00:00"
Client: "2024-02-04 17:00:01"
I didn't understand why this error happened at first. Turns out the server and client rendering results were different.
Solution:
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div>Loading...</div>;
}
return <div>Current time: {new Date().toString()}</div>;
Show "Loading..." on server, show actual time only on client.
SPA built with CRA:
bundle.js: 5.2MB 😱
→ First load on mobile: 15 seconds
→ User bounce rate: 80%
Cause:
- Imported entire moment.js (huge library)
- Imported entire lodash
- Unused components included in bundle
Solution:
1. Code Splitting: React.lazy()
2. Tree Shaking: import { format } from 'date-fns'
3. Analyze with Bundle Analyzer
4. moment.js → date-fns (lighter)
Result:
bundle.js: 500KB (↓ 90%)
First load: 3s
I thought Next.js had won, but a new war has started.
Currently, Next.js sends a lot of JavaScript during Hydration.
Problem:
- Header (static): Doesn't need JS but gets Hydrated
- Footer (static): Doesn't need JS but gets Hydrated
- Carousel (interactive): Needs JS
→ Hydrating everything makes it slow
Astro's idea:
"Hydrate only what you need"
- Header: Static HTML (0 JS)
- Footer: Static HTML (0 JS)
- Carousel: React Island (Hydrate only this)
→ JavaScript size ↓ 90%
This is Islands Architecture. It's like floating small SPA islands on an MPA ocean.
Hydration runs all components once to attach event listeners.
Qwik's idea:
"Don't run anything at all"
- Serialize event listeners into HTML
- JavaScript runs only when you click
- First Load JS = 0kb
→ Instantly interactive
When I understood this technique, I accepted that "the web keeps evolving."
✅ Admin dashboards
✅ Internal tools (like Jira, Notion)
✅ SEO completely unnecessary
✅ Pages requiring authentication
✅ Quick prototypes
✅ Blogs
✅ News sites
✅ E-commerce (Coupang, Amazon)
✅ Social media (Twitter, Instagram)
✅ Anything where SEO matters
✅ Anything where first load matters
→ As of 2025, Next.js is the right answer for 95% of projects
✅ Static blogs (minimal interaction)
✅ Documentation sites
✅ Marketing pages
✅ Portfolios
→ Faster than Next.js (almost no JavaScript)
✅ Simple blog (managed by non-developers)
✅ Don't need to know JavaScript
✅ Can solve with plugins
✅ Legacy maintenance
→ Rarely used in 2025
Looking back at my initial question:
"Why doesn't Facebook flicker?"
The answer was User Experience. Users hate waiting. They hate seeing white screens.
SPA (and hybrids) solved this problem.
We're no longer building "web pages." We're building "apps that run in browsers."
If you're starting a new project in 2025:
Don't hesitate. Start with Next.js as your default. It works perfectly in 95% of situations.
If I were to summarize what it all came down to: "The future of the web is 'fast and smooth experiences.'"