Въведение в Rendering стратегиите
Разбирането на различните rendering стратегии е критично за всеки frontend разработчик. В тази статия ще разгледаме детайлно как работят Server-Side Rendering (SSR), Static Site Generation (SSG) и Incremental Static Regeneration (ISR), с фокус върху това как Go backend-ът обработва заявките във всеки случай.
Архитектура на нашата система
Преди да разгледаме различните стратегии, нека видим как е структурирана нашата система:
┌──────────────────────────────────────────────────────────────────────────┐
│ АРХИТЕКТУРА │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Browser │ ──────► │ Nginx │ ──────► │ Astro │ │
│ │ Client │ ◄────── │ (Proxy) │ ◄────── │ (SSR) │ │
│ └─────────┘ └─────────┘ └────┬─────┘ │
│ │ │ │
│ │ │ API calls │
│ │ ▼ │
│ │ ┌──────────┐ │
│ └──────────► │ Go │ │
│ /api/* │ Backend │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ SQLite │ │
│ │ DB │ │
│ └──────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
Server-Side Rendering (SSR)
При SSR, HTML-ът се генерира на сървъра при всяка заявка. Това означава, че съдържанието винаги е актуално.
Процес на SSR заявка
┌─────────────────────────────────────────────────────────────────────────────┐
│ SSR REQUEST FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Browser 2. Nginx 3. Astro 4. Go Backend │
│ │ │ │ │ │
│ │ GET /article/5 │ │ │ │
│ │─────────────────►│ │ │ │
│ │ │ proxy_pass │ │ │
│ │ │─────────────────►│ │ │
│ │ │ │ fetch API │ │
│ │ │ │─────────────────►│ │
│ │ │ │ │ Query DB │
│ │ │ │ │──────┐ │
│ │ │ │ │ │ │
│ │ │ │ │◄─────┘ │
│ │ │ │ JSON data │ │
│ │ │ │◄─────────────────│ │
│ │ │ Render HTML │ │ │
│ │ │◄─────────────────│ │ │
│ │ Full HTML │ │ │ │
│ │◄─────────────────│ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ~50-200ms ~1ms ~10-50ms ~5-20ms │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Go Backend код за SSR заявка
// Handler за вземане на статия
func getArticleHandler(w http.ResponseWriter, r *http.Request) {
// Взимаме ID от query параметрите
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
jsonResponse(w, map[string]string{"error": "Invalid ID"}, 400)
return
}
// Query към SQLite базата
var article Article
err = db.QueryRow(`
SELECT id, title, content, author, published, render_mode,
created_at, updated_at
FROM articles
WHERE id = ? AND published = true
`, id).Scan(
&article.ID, &article.Title, &article.Content,
&article.Author, &article.Published, &article.RenderMode,
&article.CreatedAt, &article.UpdatedAt,
)
if err == sql.ErrNoRows {
jsonResponse(w, map[string]string{"error": "Not found"}, 404)
return
}
// Връщаме JSON отговор
jsonResponse(w, article, 200)
}
Astro SSR страница
---
// Този код се изпълнява на сървъра при ВСЯКА заявка
import Layout from '../layouts/Layout.astro';
import { getArticle } from '../lib/api';
const { id } = Astro.params;
const article = await getArticle(parseInt(id));
const timestamp = new Date().toISOString();
---
<Layout title={article.title}>
<article>
<h1>{article.title}</h1>
<p>Генерирано на: {timestamp}</p>
<p>Render mode: {article.render_mode}</p>
<div set:html={article.content} />
</article>
</Layout>
Кога да използвате SSR
- Персонализирано съдържание (user dashboard)
- Реално-времеви данни
- Динамични цени или наличности
- Страници с чести актуализации
Static Site Generation (SSG)
При SSG, HTML-ът се генерира по време на build процеса. Резултатът е статичен файл, който се сервира директно.
Процес на SSG заявка
┌─────────────────────────────────────────────────────────────────────────────┐
│ SSG REQUEST FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ BUILD TIME (npm run build) │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Astro Go Backend File System │
│ │ │ │ │
│ │ fetch all articles │ │ │
│ │───────────────────►│ │ │
│ │ │ Query DB │ │
│ │ JSON articles │ │ │
│ │◄───────────────────│ │ │
│ │ │ │ │
│ │ Generate HTML for each article │ │
│ │─────────────────────────────────────────►│ │
│ │ │ /article/1.html │ │
│ │ │ /article/2.html │ │
│ │ │ /article/3.html │ │
│ │
│ RUNTIME (user request) │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Browser Nginx/CDN Static Files │
│ │ │ │ │
│ │ GET /article/1 │ │ │
│ │───────────────────►│ │ │
│ │ │ Read file │ │
│ │ │────────────────────►│ │
│ │ │ HTML content │ │
│ │ │◄────────────────────│ │
│ │ Full HTML │ │ │
│ │◄───────────────────│ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ~5-20ms ~1-5ms (pre-built) │
│ │
│ ⚡ Go Backend НЕ се вика при runtime! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Важно: Контактната форма при SSG
Когато статията е SSG, страницата е статична, но контактната форма все още работи чрез JavaScript на клиента:
┌─────────────────────────────────────────────────────────────────────────────┐
│ SSG PAGE WITH CONTACT FORM │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Static HTML Page Browser JavaScript Go Backend │
│ (pre-rendered) │
│ │ │ │ │
│ │ Page loads instantly │ │ │
│ │◄──────────────────────────│ │ │
│ │ │ │ │
│ │ User fills form │ │ │
│ │ and clicks Submit │ │ │
│ │ │ │ │
│ │ JavaScript intercepts │ │ │
│ │─────────────────────────►│ │ │
│ │ │ POST /api/inquiry │ │
│ │ │────────────────────────►│ │
│ │ │ │ Save to DB │
│ │ │ {success: true} │ │
│ │ │◄────────────────────────│ │
│ │ Show success message │ │ │
│ │◄──────────────────────────│ │ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Client-side JavaScript за формата
// Този код се изпълнява в браузъра, не на сървъра
contactForm.addEventListener('submit', async (e) => {
e.preventDefault();
const inquiry = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
source_page: window.location.pathname, // '/article/5'
source_type: 'SSG', // Знаем че страницата е статична
article_id: parseInt(articleId)
};
// Директна заявка към Go backend през Nginx
const response = await fetch('/api/inquiry', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(inquiry)
});
if (response.ok) {
showSuccessMessage();
}
});
Astro SSG конфигурация
---
// export const prerender = true казва на Astro да генерира
// тази страница статично при build
export const prerender = true;
import Layout from '../layouts/Layout.astro';
import { getArticle } from '../lib/api';
const { id } = Astro.params;
const article = await getArticle(parseInt(id));
const buildTime = new Date().toISOString(); // Фиксирано при build!
---
<Layout title={article.title}>
<article>
<h1>{article.title}</h1>
<p>Генерирано при build: {buildTime}</p>
<!-- Този timestamp НИКОГА няма да се промени без rebuild -->
</article>
<!-- Контактната форма работи с client-side JS -->
<ContactForm articleId={id} renderMode="SSG" client:load />
</Layout>
Incremental Static Regeneration (ISR)
ISR комбинира предимствата на SSG (бързина) и SSR (свежи данни). Страницата е статична, но се регенерира периодично.
ISR процес
┌─────────────────────────────────────────────────────────────────────────────┐
│ ISR REQUEST FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ First Request (cache empty) │
│ ───────────────────────────────────────────────────────────────────────── │
│ │ │
│ │ Same as SSR - generate and cache │
│ ▼ │
│ │
│ Subsequent Requests (within TTL) │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Browser Cache Go Backend │
│ │ │ │ │
│ │ GET /article/1 │ │ │
│ │──────────────────►│ │ │
│ │ │ Cache HIT │ │
│ │ Cached HTML │ (no backend call) │ │
│ │◄──────────────────│ │ │
│ │
│ After TTL expires (stale-while-revalidate) │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Browser Cache Go Backend │
│ │ │ │ │
│ │ GET /article/1 │ │ │
│ │──────────────────►│ │ │
│ │ Stale HTML │ │ │
│ │◄──────────────────│ │ │
│ │ │ Background refresh │ │
│ │ │─────────────────────►│ │
│ │ │ Fresh data │ │
│ │ │◄─────────────────────│ │
│ │ │ Update cache │ │
│ │
│ Next request gets fresh HTML │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Astro ISR имплементация
// В Astro няма native ISR, но можем да го симулираме:
const CACHE_TTL = 30; // секунди
const cache = new Map();
async function getArticleWithCache(id: number) {
const cacheKey = `article_${id}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL * 1000) {
return cached.data;
}
// Fetch fresh data
const article = await fetch(`${API_BASE}/api/article?id=${id}`)
.then(r => r.json());
cache.set(cacheKey, {
data: article,
timestamp: Date.now()
});
return article;
}
Сравнение на стратегиите
┌────────────────┬─────────────┬─────────────┬─────────────┬─────────────────┐
│ Критерий │ SSR │ SSG │ ISR │ Препоръка │
├────────────────┼─────────────┼─────────────┼─────────────┼─────────────────┤
│ TTFB │ Бавен │ Много бърз│ Бърз │ SSG за статични │
│ │ (50-200ms) │ (5-20ms) │ (5-50ms) │ │
├────────────────┼─────────────┼─────────────┼─────────────┼─────────────────┤
│ Данни │ Винаги │ От build │ Периодично │ SSR за динам. │
│ актуалност │ свежи │ момента │ обновявани │ │
├────────────────┼─────────────┼─────────────┼─────────────┼─────────────────┤
│ Сървърно │ Високо │ Нулево │ Ниско │ SSG за трафик │
│ натоварване │ │ │ │ │
├────────────────┼─────────────┼─────────────┼─────────────┼─────────────────┤
│ CDN кеширане │ Трудно │ Лесно │ Лесно │ SSG/ISR за CDN │
├────────────────┼─────────────┼─────────────┼─────────────┼─────────────────┤
│ SEO │ Отлично │ Отлично │ Отлично │ Всички са добри │
├────────────────┼─────────────┼─────────────┼─────────────┼─────────────────┤
│ Персонализ. │ Да │ Не │ Не │ SSR за auth │
└────────────────┴─────────────┴─────────────┴─────────────┴─────────────────┘
Автоматичен Rebuild при SSG
В нашата система, когато се промени SSG статия, Go backend-ът автоматично тригерира rebuild:
// triggerRebuild се вика когато SSG статия се промени
func triggerRebuild() error {
log.Println("Triggering Astro rebuild...")
// Изпълняваме npm run build
cmd := exec.Command("bash", "-c",
"cd /var/www/go-astro-1.pasifora.com/frontend && npm run build")
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Rebuild error: %v\nOutput: %s", err, string(output))
return err
}
log.Println("Rebuild completed successfully")
// Рестартираме Node.js сървъра
exec.Command("bash", "-c",
"pkill -f 'entry.mjs'").Run()
go func() {
time.Sleep(1 * time.Second)
exec.Command("bash", "-c",
"cd /var/www/.../frontend && "+
"HOST=127.0.0.1 PORT=4322 "+
"nohup node ./dist/server/entry.mjs &").Run()
}()
return nil
}
Заключение
Изборът между SSR, SSG и ISR зависи от конкретните нужди:
- SSG: Блогове, документация, маркетингови страници
- SSR: Dashboards, e-commerce с динамични цени, персонализирано съдържание
- ISR: Новинарски сайтове, продуктови каталози с редки обновявания
Важното е да разбирате как работи всяка стратегия и как данните текат от браузъра през различните слоеве до базата данни и обратно.
Брой думи: 3,412