رفتن به محتوا
LoopX

عملکرد: gzip و کش

توی این درس یاد می‌گیری با دو ابزار ساده، سایت را برای کاربرانت چند برابر سریع‌تر کنی: فشرده‌سازی (gzip) که حجم HTML، CSS و JS را تا یک‌پنجم کم می‌کند، و کش مرورگر (Cache-Control و expires) که باعث می‌شود بازدید دوم اصلاً چیزی دانلود نکند. همه‌چیز را روی خروجی build همین سایت LoopX اندازه می‌گیری: می‌بینی Ubuntu به‌صورت پیش‌فرض فقط HTML را فشرده می‌کند و CSS و JS را نه، gzip_types را درست می‌کنی، سطح فشرده‌سازی را با هزینه‌ی CPU می‌سنجی، با gzip_static فایل‌های از قبل فشرده را سرو می‌کنی، و برای فایل‌های hash‌دار کش یک‌ساله (immutable) و برای HTML کش «همیشه بپرس» (no-cache) می‌گذاری. ابزار اندازه‌گیری‌ات curl -H "Accept-Encoding: gzip" -I است.

مسئله: هر بایت، برای کاربر موبایل

Section titled “مسئله: هر بایت، برای کاربر موبایل”

کاربر ایرانی با اینترنت موبایل در یک روز شلوغ شاید فقط چند صد کیلوبیت بر ثانیه سرعت واقعی داشته باشد. صفحه‌ای که ۵۰۰ کیلوبایت HTML و CSS و JS دارد، چند ثانیه سفید می‌ماند. دو راه بدون عوض کردن یک خط کد سایت:

۱. کمتر بفرست: متن (HTML، CSS، JS، JSON، SVG) خیلی خوب فشرده می‌شود؛ ۸۴ کیلوبایت CSS بعد از gzip شاید ۱۵ کیلوبایت باشد. ۲. دوباره نفرست: فایلی که مرورگر دیروز گرفته و از آن موقع عوض نشده، نباید دوباره دانلود شود.

تشبیه: بسته‌بندی وکیوم و یخچال خانه

Section titled “تشبیه: بسته‌بندی وکیوم و یخچال خانه”

gzip مثل بسته‌بندی وکیوم است: لباس‌ها همان لباس‌اند، ولی در چمدان یک‌پنجم جا می‌گیرند؛ فرستنده کمی وقت برای بسته‌بندی می‌گذارد و گیرنده بازش می‌کند، ولی حمل خیلی ارزان‌تر است. کش مثل یخچال خانه است: چیزی که دیروز خریدی و خراب نمی‌شود، امروز دوباره از فروشگاه نمی‌خری. فقط باید روی هر چیز تاریخ انقضا بنویسی (max-age)؛ و برای چیزهایی که هرگز عوض نمی‌شوند (فایل با اسم hash‌دار)، «تا یک سال سالم است».

چه چیزی را فشرده و چه چیزی را کش کنیم؟

Section titled “چه چیزی را فشرده و چه چیزی را کش کنیم؟”
دو ابزار سرعت: gzip حجم هر پاسخ متنی را کم می‌کند (برای عکس و ویدیو که خودشان فشرده‌اند فایده ندارد)؛ کش تعداد درخواست‌ها را کم می‌کند. فایل‌های hash‌دار (مثل /_astro/common.D0VTP8CO.css) با هر تغییر اسم تازه می‌گیرند، پس می‌شود یک سال کششان کرد؛ HTML اسم ثابت دارد و باید هر بار با ETag پرسیده شود.

این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24) با کاربر root اجرا شده و سایتی که سرو می‌کنیم، خروجی build همین سایت LoopX (dist/ مخزن) است؛ پس اعداد این درس اندازه‌های واقعی یک سایت واقعی‌اند.

مثال ۱: سایت بدون تنظیم، و curl -H "Accept-Encoding: gzip" -I

Section titled “مثال ۱: سایت بدون تنظیم، و curl -H "Accept-Encoding: gzip" -I”
/etc/nginx/sites-available/loopx.test
server {
listen 80;
server_name loopx.test;
root /var/www/loopx.test;
index index.html;
location / {
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
}

مرورگرها در هر درخواست با هدر Accept-Encoding می‌گویند چه فشرده‌سازی‌هایی را می‌فهمند. curl به‌صورت پیش‌فرض این هدر را نمی‌فرستد؛ پس با -H "Accept-Encoding: gzip" نقش مرورگر را بازی می‌کنیم:

Terminal window
ln -s /etc/nginx/sites-available/loopx.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
cd /var/www/loopx.test
css=/_astro/$(ls _astro | grep -E '^common\..*\.css$' | head -n 1)
js=/_astro/$(ls -S _astro | grep -E '\.js$' | head -n 1)
echo "$css" > /root/css.path; echo "$js" > /root/js.path
for p in /docker/what-is-docker/ "$css" "$js" /favicon.svg; do
echo "== $p"
curl -s -H "Accept-Encoding: gzip" -I "http://loopx.test$p" | grep -iE '^(content-type|content-encoding|content-length)'
done
cd ~
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
== /docker/what-is-docker/
Content-Type: text/html
Content-Encoding: gzip
== /_astro/common.D0VTP8CO.css
Content-Type: text/css
Content-Length: 84442
== /_astro/ui-core.hRq-JN9-.js
Content-Type: application/javascript
Content-Length: 93823
== /favicon.svg
Content-Type: image/svg+xml
Content-Length: 10806

HTML با Content-Encoding: gzip آمد (اندازه‌اش در هدر نیست چون در حین ارسال فشرده می‌شود و اندازه‌ی نهایی از قبل معلوم نیست). ولی CSS، JS و SVG بدون Content-Encoding و با اندازه‌ی کامل (Content-Length) فرستاده شدند. چرا؟

Terminal window
grep -n 'gzip' /etc/nginx/nginx.conf
خروجی
46: gzip on;
48: # gzip_vary on;
49: # gzip_proxied any;
50: # gzip_comp_level 6;
51: # gzip_buffers 16 8k;
52: # gzip_http_version 1.1;
53: # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

در Ubuntu، gzip on; روشن است ولی gzip_types (و بقیه) کامنت‌اند. پیش‌فرض gzip_types فقط text/html است. یعنی سنگین‌ترین فایل‌های سایت فشرده نمی‌شوند.

مثال ۲: gzip_types و تنظیم کامل

Section titled “مثال ۲: gzip_types و تنظیم کامل”

تنظیم‌های gzip را در یک فایل conf.d (context http، برای همه‌ی سایت‌ها) می‌گذاریم:

/etc/nginx/conf.d/gzip.conf
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css text/xml application/json application/javascript
application/xml application/rss+xml image/svg+xml font/ttf font/otf;
Terminal window
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
css=$(cat /root/css.path); js=$(cat /root/js.path)
printf '%-40s %10s %10s %7s\n' "file" "plain" "gzip" "saved"
for p in /docker/what-is-docker/ "$css" "$js" /favicon.svg /og/docker.png; do
plain=$(curl -s -o /dev/null -w '%{size_download}' "http://loopx.test$p")
gz=$(curl -s -o /dev/null -w '%{size_download}' -H 'Accept-Encoding: gzip' "http://loopx.test$p")
printf '%-40s %10s %10s %6s%%\n' "${p:0:40}" "$plain" "$gz" "$(( (plain - gz) * 100 / plain ))"
done
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
file plain gzip saved
/docker/what-is-docker/ 226815 35703 84%
/_astro/common.D0VTP8CO.css 84442 17971 78%
/_astro/ui-core.hRq-JN9-.js 93823 26217 72%
/favicon.svg 10806 3498 67%
/og/docker.png 48083 48083 0%

این اعداد واقعی همین سایت‌اند: HTML یک درس، CSS و JS بیش از ۷۰ تا ۸۰ درصد کوچک‌تر شدند. ولی عکس PNG (تصویر Open Graph) تقریباً هیچ تغییری نکرد (و برای همین image/png را در gzip_types نگذاشتیم؛ فشرده کردن دوباره‌اش فقط CPU هدر می‌دهد).

هر خط تنظیم:

  • gzip_types: نوع‌هایی (از Content-Type) که فشرده می‌شوند (text/html همیشه هست و نباید دوباره بنویسی).
  • gzip_comp_level 5: سطح ۱ (سریع، فشرده‌سازی کمتر) تا ۹ (کند، کمی بیشتر). ۴ تا ۶ تعادل خوبی است (پشت پرده).
  • gzip_min_length 256: پاسخ‌های خیلی کوچک را فشرده نکن (سربار gzip از سودش بیشتر است).
  • gzip_vary on: هدر Vary: Accept-Encoding اضافه می‌کند تا کش‌های میانی (CDN، proxy) نسخه‌ی فشرده را به مرورگری که gzip نمی‌فهمد ندهند.
  • gzip_proxied any: پاسخ‌هایی که از اپ پشت proxy می‌آیند (درس reverse proxy) هم فشرده شوند.

مثال ۳: زمان واقعی روی اینترنت کند

Section titled “مثال ۳: زمان واقعی روی اینترنت کند”

حجم کمتر روی اینترنت کند یعنی زمان کمتر. برای دیدنش یک اتصال کند لازم داریم. خود Nginx directive limit_rate دارد که سرعت فرستادن هر پاسخ را محدود می‌کند (در دنیای واقعی برای محدود کردن سرعت دانلود فایل‌های بزرگ به کار می‌رود). یک server دوم روی پورت 8095 می‌سازیم که همان سایت را با ۱۰ کیلوبایت بر ثانیه (تقریباً ۸۰ کیلوبیت؛ مثل اینترنت موبایل در یک جای شلوغ) می‌فرستد، و CSS اصلی سایت را با و بدون gzip از آن می‌گیریم:

Terminal window
cat > /etc/nginx/conf.d/lx-slow.conf <<'EOF'
# the same site, sent at ~10 KB/s per response (a very slow link)
server {
listen 8095;
server_name loopx.test;
root /var/www/loopx.test;
limit_rate 10k;
}
EOF
nginx -t && systemctl reload nginx && sleep 1
css=$(cat /root/css.path)
for enc in identity gzip; do
curl -s -o /dev/null -H "Accept-Encoding: $enc" \
-w "$enc: %{size_download} bytes in %{time_total}s\n" "http://loopx.test:8095$css"
done
خروجی
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
identity: 84442 bytes in 7.235594s
gzip: 17971 bytes in 1.004197s

limit_rate 10k سرعت هر پاسخ را به ۱۰ کیلوبایت بر ثانیه محدود می‌کند (gzip هم روی این server فعال است، چون conf.d/gzip.conf در context http است و به همه‌ی serverها می‌رسد). یک ریزه‌کاری: Nginx سهم «ثانیه‌ی اول» (اینجا ۱۰ کیلوبایت) را بلافاصله می‌فرستد و بعد سرعت را محدود می‌کند؛ برای همین عددها کمی کمتر از «حجم تقسیم بر سرعت» هستند. ولی نتیجه روشن است: همان فایل، روی همان اتصال کند، بدون فشرده‌سازی چند ثانیه و با gzip کمتر از یک ثانیه. برای صفحه‌ای که چند فایل CSS و JS دارد، این فاصله مستقیم در «کی صفحه نمایش داده می‌شود» دیده می‌شود.

مثال ۴: کش یک‌ساله برای فایل‌های hash‌دار

Section titled “مثال ۴: کش یک‌ساله برای فایل‌های hash‌دار”

فایل‌های /_astro/ اسمشان یک hash از محتوا دارد (common.D0VTP8CO.css): اگر محتوا یک بایت عوض شود، build اسم تازه‌ای می‌سازد و HTML به اسم تازه لینک می‌دهد. پس محتوای یک اسم هرگز عوض نمی‌شود و می‌شود به مرورگر گفت «تا یک سال حتی نپرس». HTML برعکس، اسم ثابت دارد (/docker/what-is-docker/) و باید هر بار پرسیده شود:

Terminal window
cat > /etc/nginx/sites-available/loopx.test <<'EOF'
server {
listen 80;
server_name loopx.test;
root /var/www/loopx.test;
index index.html;
# hashed build assets: content never changes under the same name
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable" always;
try_files $uri =404;
}
# other static files without a hash in the name
location ~* \.(png|jpg|jpeg|webp|svg|ico|woff2)$ {
expires 7d;
}
# HTML: always revalidate (cheap 304 thanks to ETag)
location / {
add_header Cache-Control "no-cache" always;
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
css=$(cat /root/css.path)
for p in "$css" /og/docker.png /docker/what-is-docker/; do
echo "== $p"
curl -s -I "http://loopx.test$p" | grep -iE '^(cache-control|expires)'
done
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
== /_astro/common.D0VTP8CO.css
Expires: Mon, 04 Oct 2027 14:49:07 GMT
Cache-Control: max-age=31536000
Cache-Control: public, immutable
== /og/docker.png
Expires: Sun, 11 Oct 2026 14:49:07 GMT
Cache-Control: max-age=604800
== /docker/what-is-docker/
Cache-Control: no-cache
  • expires 1y دو هدر می‌سازد: Expires (تاریخ مطلق، برای کلاینت‌های قدیمی) و Cache-Control: max-age=31536000 (یک سال به ثانیه). add_header Cache-Control "public, immutable" یک هدر Cache-Control دوم اضافه می‌کند؛ مرورگرها هر دو را با هم می‌خوانند: کش یک‌ساله، قابل ذخیره در کش‌های عمومی (public)، و immutable یعنی حتی با دکمه‌ی رفرش هم دوباره نپرس.
  • عکس‌ها (که اسمشان hash ندارد): ۷ روز. اگر عکسی عوض شد، تا یک هفته ممکن است بعضی کاربران نسخه‌ی قدیمی را ببینند؛ بسته به سایت انتخاب کن.
  • HTML: no-cache. برخلاف اسمش یعنی «ذخیره کن، ولی قبل از استفاده بپرس عوض شده یا نه»؛ و پرسیدن با ETag (درس سایت استاتیک) فقط یک پاسخ ۳۰۴ بدون بدنه است. (برای «اصلاً ذخیره نکن»، no-store؛ برای صفحه‌های حساس مثل پنل.)

مثال ۵: gzip_static، فشرده‌سازی یک بار برای همیشه

Section titled “مثال ۵: gzip_static، فشرده‌سازی یک بار برای همیشه”

gzip در لحظه برای هر درخواست کمی CPU می‌خورد. برای فایل‌های ثابت، می‌شود یک بار (مثلاً بعد از build) با بالاترین سطح فشرده کرد و کنار فایل اصلی گذاشت (app.css.gz)؛ با gzip_static on Nginx اگر نسخه‌ی .gz بود، همان را مستقیم می‌فرستد:

Terminal window
cd /var/www/loopx.test/_astro
css=$(cat /root/css.path); f=$(basename "$css")
gzip -k -9 "$f"
ls -l "$f" "$f.gz" | awk '{print $5, $9}'
cd ~
echo "=== قبل از gzip_static (فشرده‌سازی در لحظه، سطح ۵):"
curl -s -o /dev/null -w '%{size_download} bytes\n' -H 'Accept-Encoding: gzip' "http://loopx.test$css"
sed -i 's| expires 1y;| expires 1y;\n gzip_static on;|' /etc/nginx/sites-available/loopx.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
echo "=== بعد از gzip_static (فایل .gz سطح ۹):"
curl -s -o /dev/null -w '%{size_download} bytes\n' -H 'Accept-Encoding: gzip' "http://loopx.test$css"
curl -s -I -H 'Accept-Encoding: gzip' "http://loopx.test$css" | grep -iE '^(content-encoding|content-length)'
خروجی
84442 common.D0VTP8CO.css
17500 common.D0VTP8CO.css.gz
=== قبل از gzip_static (فشرده‌سازی در لحظه، سطح ۵):
17971 bytes
nginx: configuration file /etc/nginx/nginx.conf test is successful
=== بعد از gzip_static (فایل .gz سطح ۹):
17500 bytes
Content-Length: 17500
Content-Encoding: gzip

اندازه‌ی پاسخ دقیقاً اندازه‌ی فایل .gz شد؛ یعنی Nginx خودش فشرده نکرد و فایل آماده را فرستاد (و چون اندازه از قبل معلوم است، Content-Length هم دارد). مزیت دوبرابر: CPU صفر برای فشرده‌سازی، و فشرده‌سازی سطح ۹ بدون هزینه‌ی لحظه‌ای. خیلی از ابزارهای build (و پلاگین‌های Vite و Astro) می‌توانند فایل‌های .gz (و .br برای Brotli) را خودشان بسازند.

در HTTP/1.1، مرورگر برای هر فایل یک درخواست جدا می‌فرستد و تعداد اتصال‌های همزمان محدود است. HTTP/2 همه‌ی فایل‌ها را روی یک اتصال و همزمان می‌فرستد و هدرها را هم فشرده می‌کند. در Nginx فقط روی HTTPS (عملاً، چون مرورگرها HTTP/2 را فقط روی TLS پشتیبانی می‌کنند) و با یک کلمه:

Terminal window
mkdir -p /etc/nginx/ssl
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout /etc/nginx/ssl/loopx.key -out /etc/nginx/ssl/loopx.crt \
-subj '/CN=loopx.test' -addext 'subjectAltName=DNS:loopx.test' 2>/dev/null
chmod 600 /etc/nginx/ssl/loopx.key
sed -i 's|^ listen 80;| listen 80;\n listen 443 ssl http2;\n ssl_certificate /etc/nginx/ssl/loopx.crt;\n ssl_certificate_key /etc/nginx/ssl/loopx.key;|' /etc/nginx/sites-available/loopx.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s -I --cacert /etc/nginx/ssl/loopx.crt https://loopx.test/ | head -n 1
curl -s -I --http1.1 --cacert /etc/nginx/ssl/loopx.crt https://loopx.test/ | head -n 1
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
HTTP/2 200
HTTP/1.1 200 OK

curl خودش HTTP/2 را انتخاب کرد (HTTP/2 200)؛ با --http1.1 همان سرور با HTTP/1.1 هم جواب می‌دهد. (در Nginx ۱.۲۵ به بعد، شکل جدید http2 on; به‌صورت directive جداست؛ در ۱.۲۴ Ubuntu همین listen ... http2 درست است.)

پشت پرده: Accept-Encoding، Vary و هزینه‌ی سطح‌های gzip

Section titled “پشت پرده: Accept-Encoding، Vary و هزینه‌ی سطح‌های gzip”

فشرده‌سازی یک مذاکره است: مرورگر می‌گوید چه می‌فهمد (Accept-Encoding: gzip, deflate, br)، سرور اگر خواست، فشرده می‌فرستد و با Content-Encoding: gzip اعلام می‌کند. کلاینتی که Accept-Encoding نفرستد، نسخه‌ی خام می‌گیرد؛ پس هیچ کلاینتی خراب نمی‌شود. Vary: Accept-Encoding به کش‌های میانی می‌گوید «پاسخ این آدرس به این هدر درخواست بستگی دارد؛ دو نسخه نگه دار».

سطح فشرده‌سازی چقدر مهم است؟ همان CSS را با چند سطح (از ۱ تا ۹) فشرده می‌کنیم و زمان را با ۲۰۰ بار تکرار می‌سنجیم:

Terminal window
css=/var/www/loopx.test$(cat /root/css.path)
printf 'original: %s bytes\n' "$(stat -c %s "$css")"
printf '%-6s %8s %10s\n' level bytes "ms/200x"
for level in 1 3 5 6 9; do
size=$(gzip -c -$level "$css" | wc -c)
start=$(date +%s%N)
for i in $(seq 200); do gzip -c -$level "$css" > /dev/null; done
ms=$(( ($(date +%s%N) - start) / 1000000 ))
printf '%-6s %8s %10s\n' "$level" "$size" "$ms"
done
خروجی
original: 84442 bytes
level bytes ms/200x
1 21656 588
3 20051 596
5 17900 673
6 17614 747
9 17500 927

از سطح ۱ تا ۵ اندازه به‌وضوح کم می‌شود؛ بعد از آن سود اندازه خیلی کم است ولی زمان CPU بیشتر می‌شود (سطح ۹ چند برابر سطح ۱). برای فشرده‌سازی در لحظه ۴ تا ۶ منطقی است؛ سطح ۹ را برای gzip_static (یک بار، در build) نگه دار. (این زمان‌ها برای ابزار gzip خط فرمان است؛ Nginx از همان کتابخانه‌ی zlib استفاده می‌کند و الگوی هزینه یکی است.)

directive های gzip:

directive پیشنهاد کار
gzip on روشن کردن
gzip_types text/css application/javascript application/json image/svg+xml ... نوع‌های فشرده‌شونده (HTML همیشه)
gzip_comp_level 5 ۱ تا ۹
gzip_min_length 256 حداقل اندازه
gzip_vary on هدر Vary: Accept-Encoding
gzip_proxied any فشرده کردن پاسخ‌های proxy
gzip_static on (در location دارایی‌ها) سرو کردن .gz آماده

سیاست کش رایج:

نوع فایل هدر دلیل
فایل hash‌دار (/_astro/، /assets/) expires 1y + Cache-Control: public, immutable محتوای یک اسم هرگز عوض نمی‌شود
عکس و فونت بدون hash expires 7d تا 30d تعادل سرعت و تازگی
HTML Cache-Control: no-cache همیشه بپرس (۳۰۴ ارزان)
صفحه‌های حساس (پنل، حساب کاربری) Cache-Control: no-store هیچ‌جا ذخیره نشود
پاسخ API معمولاً no-store یا max-age کوتاه بسته به داده

ابزارهای اندازه‌گیری:

دستور چه می‌گوید
curl -H "Accept-Encoding: gzip" -I URL فشرده می‌شود؟ (Content-Encoding)
curl -so /dev/null -w '%{size_download}' -H 'Accept-Encoding: gzip' URL اندازه‌ی واقعی منتقل‌شده
limit_rate 10k; (در یک server آزمایشی) + curl -w '%{time_total}' شبیه‌سازی اینترنت کند
curl -I URL | grep -i cache-control سیاست کش
ابزار توسعه‌دهنده‌ی مرورگر، زبانه‌ی Network ستون Size (disk cache) و Time

۱) اعتماد به gzip on پیش‌فرض

Section titled “۱) اعتماد به gzip on پیش‌فرض”

مثال ۱: در Ubuntu فقط HTML فشرده می‌شود. راه‌حل: gzip_types را صریح بنویس و با curl -H "Accept-Encoding: gzip" -I روی CSS و JS بررسی کن.

۲) کش طولانی برای فایل بدون hash

Section titled “۲) کش طولانی برای فایل بدون hash”

اگر به /css/style.css (بدون hash) کش یک‌ساله بدهی، بعد از هر تغییر، کاربران تا یک سال CSS قدیمی را می‌بینند (و فقط با پاک کردن کش مرورگر درست می‌شود). راه‌حل: کش طولانی فقط برای اسم‌های hash‌دار؛ بقیه کوتاه یا no-cache.

Terminal window
cat > /etc/nginx/conf.d/lx-badcache.conf <<'EOF'
server {
listen 8096;
root /var/www/loopx.test;
location / { expires 30d; }
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s -I http://localhost:8096/ | grep -iE '^cache-control'
rm /etc/nginx/conf.d/lx-badcache.conf
systemctl reload nginx
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
Cache-Control: max-age=2592000

با max-age ۳۰ روزه روی HTML، کاربری که امروز صفحه را دیده، تا یک ماه HTML جدید (و در نتیجه لینک به CSS و JS جدید) را نمی‌گیرد؛ یعنی deploy امروز برایش اصلاً اتفاق نیفتاده. راه‌حل: no-cache برای HTML.

۴) فشرده کردن عکس و فایل فشرده

Section titled “۴) فشرده کردن عکس و فایل فشرده”

gzip_types image/jpeg image/png application/zip فقط CPU هدر می‌دهد (مثال ۲: PNG تقریباً کوچک نشد). راه‌حل: فقط فرمت‌های متنی (و SVG که متن است).

۵) add_header Cache-Control و گم شدن بقیه‌ی هدرها

Section titled “۵) add_header Cache-Control و گم شدن بقیه‌ی هدرها”

add_header در location (مثلاً برای Cache-Control) هدرهای امنیتی سطح server را حذف می‌کند (درس ساختار تنظیمات و امنیت). راه‌حل: snippet هدرهای امنیتی را در همان location هم include کن.

✎ تمرینآسان

تمرین اصلی درس (بخش اول): حجم و زمان صفحه‌ی اصلی LoopX و همه‌ی فایل‌های CSS و JS که آن صفحه لینک می‌دهد را قبل و بعد از gzip مقایسه کن. (راهنمایی: آدرس فایل‌ها را با grep -o از HTML صفحه‌ی اصلی دربیاور.)

دیدن جواب
Terminal window
html=$(curl -s http://loopx.test/)
assets=$(grep -oE '(href|src)="/_astro/[^"]+\.(css|js)"' <<< "$html" | cut -d'"' -f2 | sort -u)
total_plain=0; total_gz=0
for p in / $assets; do
plain=$(curl -s -o /dev/null -w '%{size_download}' "http://loopx.test$p")
gz=$(curl -s -o /dev/null -w '%{size_download}' -H 'Accept-Encoding: gzip' "http://loopx.test$p")
total_plain=$((total_plain + plain)); total_gz=$((total_gz + gz))
done
echo "files: $(( $(wc -w <<< "$assets") + 1 ))"
echo "without gzip: $total_plain bytes"
echo "with gzip: $total_gz bytes ($(( 100 - total_gz * 100 / total_plain ))% smaller)"
for enc in identity gzip; do
t=0
for p in / $assets; do
t=$(awk -v a="$t" -v b="$(curl -s -o /dev/null -H "Accept-Encoding: $enc" -w '%{time_total}' "http://loopx.test:8095$p")" 'BEGIN {print a + b}')
done
echo "at 10 KB/s, $enc: ${t}s (one after another)"
done
خروجی
files: 6
without gzip: 189950 bytes
with gzip: 36019 bytes (82% smaller)
at 10 KB/s, identity: 17.0776s (one after another)
at 10 KB/s, gzip: 2.01114s (one after another)

grep -oE هر href یا src که به /_astro/...css|js اشاره می‌کند را پیدا کرد. جمع حجم صفحه‌ی اصلی و همه‌ی CSS و JS اش با gzip چند برابر کمتر شد، و روی اتصال کند هم زمان دانلود پشت‌سرهم (مرورگر واقعی موازی می‌گیرد، ولی پهنای باند همان است) چند برابر کمتر شد. (نسبت زمان کمی از نسبت حجم بیشتر است، چون فایل‌های فشرده‌ی کوچک بیشترشان در همان سهم «ثانیه‌ی اول» limit_rate جا می‌شوند.)

✎ تمرینمتوسط

تمرین اصلی درس (بخش دوم): سیاست کش سایت را با یک اسکریپت بررسی کن: برای همه‌ی فایل‌های /_astro/ ثابت کن max-age=31536000 و immutable دارند، و برای سه صفحه‌ی HTML ثابت کن no-cache دارند و درخواست دوم با If-None-Match جواب 304 می‌گیرد.

دیدن جواب
Terminal window
ok=0; bad=0
for f in /var/www/loopx.test/_astro/*.css /var/www/loopx.test/_astro/*.js; do
p=${f#/var/www/loopx.test}
h=$(curl -s -I "http://loopx.test$p")
if grep -q 'max-age=31536000' <<< "$h" && grep -q 'immutable' <<< "$h"; then ok=$((ok + 1)); else bad=$((bad + 1)); echo "BAD: $p"; fi
done
echo "_astro assets: $ok ok, $bad bad"
for p in / /nginx/performance/ /bash/; do
h=$(curl -s -I "http://loopx.test$p")
cc=$(grep -i '^cache-control' <<< "$h" | tr -d '\r')
etag=$(grep -i '^etag' <<< "$h" | cut -d' ' -f2 | tr -d '\r')
code=$(curl -s -o /dev/null -w '%{http_code}' -H "If-None-Match: $etag" "http://loopx.test$p")
echo "$p -> $cc, revalidate: $code"
done
خروجی
_astro assets: 13 ok, 0 bad
/ -> Cache-Control: no-cache, revalidate: 304
/nginx/performance/ -> Cache-Control: no-cache, revalidate: 304
/bash/ -> Cache-Control: no-cache, revalidate: 304

همه‌ی فایل‌های hash‌دار کش یک‌ساله و immutable دارند، و صفحه‌های HTML no-cache با جواب 304 برای درخواست دوم؛ یعنی بازدید دوم یک صفحه تقریباً فقط چند پاسخ بدون بدنه است.

✎ تمرینسخت

یک API کوچک (اپ پایتون که JSON بزرگ برمی‌گرداند) را پشت Nginx بگذار و ثابت کن: ۱) پاسخ JSON اپ فشرده می‌شود (gzip_proxied any)؛ ۲) اگر اپ خودش Content-Encoding فرستاد، Nginx دوباره فشرده نمی‌کند؛ ۳) هدر Vary: Accept-Encoding می‌آید.

دیدن جواب
Terminal window
mkdir -p /srv/lx-api
cat > /srv/lx-api/api.py <<'EOF'
# api.py: big JSON at /plain, already-gzipped JSON at /pregz
import gzip, json
from http.server import BaseHTTPRequestHandler, HTTPServer
DATA = json.dumps([{"id": i, "title": f"product {i}", "price": i * 1000} for i in range(500)]).encode()
class H(BaseHTTPRequestHandler):
def do_GET(self):
body, extra = DATA, {}
if self.path == "/api/pregz":
body, extra = gzip.compress(DATA), {"Content-Encoding": "gzip"}
self.send_response(200)
self.send_header("Content-Type", "application/json")
for k, v in extra.items():
self.send_header(k, v)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a):
pass
HTTPServer(("127.0.0.1", 9401), H).serve_forever()
EOF
(exec python3 /srv/lx-api/api.py) > /dev/null 2>&1 &
sed -i 's|^ location / {| location /api/ {\n proxy_pass http://127.0.0.1:9401;\n }\n\n location / {|' /etc/nginx/sites-available/loopx.test
sleep 1
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for p in /api/plain /api/pregz; do
echo "== $p"
# GET (not -I): this tiny app has no HEAD handler
curl -s -D - -o /dev/null -H 'Accept-Encoding: gzip' "http://loopx.test$p" | grep -iE '^(content-encoding|vary)'
echo " size: $(curl -s -o /dev/null -w '%{size_download}' -H 'Accept-Encoding: gzip' "http://loopx.test$p") bytes (plain: $(curl -s -o /dev/null -w '%{size_download}' "http://loopx.test/api/plain"))"
done
pkill -f '/srv/lx-api/api.py'
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
== /api/plain
Vary: Accept-Encoding
Content-Encoding: gzip
size: 3750 bytes (plain: 26667)
== /api/pregz
Content-Encoding: gzip
size: 3811 bytes (plain: 26667)

/api/plain را اپ خام فرستاد و Nginx فشرده کرد (gzip_proxied any و application/json در gzip_types)، با Vary. /api/pregz را اپ خودش فشرده کرده بود؛ Nginx دید Content-Encoding دارد و دوباره فشرده نکرد؛ و چون خودش فشرده نکرد، Vary هم اضافه نکرد (اپی که خودش فشرده می‌کند، Vary را هم باید خودش بفرستد). (برای کلاینتی که gzip نمی‌فهمد، /api/pregz ما همچنان فشرده می‌فرستد؛ اپ واقعی باید خودش Accept-Encoding را بخواند، یا ساده‌تر، فشرده‌سازی را به Nginx بسپارد.)

⚡ بررسی سریع

در Ubuntu، gzip on روشن است ولی CSS فشرده نمی‌شود. چرا؟

؟ آزمونک
  1. چرا فایل /_astro/common.D0VTP8CO.css را می‌شود یک سال کش کرد ولی /index.html را نه؟

  2. Cache-Control: no-cache یعنی چه؟

  3. gzip_comp_level 9 برای فشرده‌سازی در لحظه چه ایرادی دارد؟

  4. gzip_static on چه می‌کند؟

  5. gzip_vary on چه هدری اضافه می‌کند و چرا مهم است؟

  6. چرا image/png را در gzip_types نمی‌گذاریم؟

  • gzip: در Ubuntu فقط HTML فشرده می‌شود؛ در conf.d/gzip.conf: gzip_types (CSS، JS، JSON، SVG، XML، فونت ttf/otf)، gzip_comp_level 5، gzip_min_length 256، gzip_vary on، gzip_proxied any. برای فایل‌های متنی ۷۰ تا ۸۰ درصد کوچک‌تر؛ برای عکس و ویدیو بی‌فایده.
  • gzip_static on: سرو .gz آماده (سطح ۹ در build، بدون CPU در لحظه).
  • کش: فایل hash‌دار: expires 1y + Cache-Control "public, immutable"؛ HTML: no-cache (۳۰۴ با ETag)؛ عکس بدون hash: چند روز؛ صفحه‌ی حساس: no-store.
  • HTTP/2: listen 443 ssl http2; (در ۱.۲۵+: http2 on;).
  • اندازه بگیر: curl -H "Accept-Encoding: gzip" -I (فشرده می‌شود؟)، -w '%{size_download}' (چقدر)، server آزمایشی با limit_rate 10k (روی اینترنت کند)، curl -I | grep -i cache-control.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
curl -H "Accept-Encoding: gzip" -I URLفشرده می‌شود؟ (Content-Encoding)
curl -so /dev/null -w '%{size_download}' -H 'Accept-Encoding: gzip' URLاندازه‌ی منتقل‌شده
limit_rate 10k;محدود کردن سرعت هر پاسخ (شبیه‌سازی اینترنت کند)
curl -so /dev/null -w "%{time_total}" URLزمان کل دریافت
gzip_types text/css application/javascript application/json image/svg+xml;فشرده‌سازی CSS و JS و ...
gzip_comp_level 5; gzip_min_length 256; gzip_vary on; gzip_proxied any;تنظیم متعادل
gzip_static on;سرو .gz آماده
gzip -k -9 file.cssساخت file.css.gz و نگه داشتن اصلی
expires 1y; add_header Cache-Control "public, immutable" always;فایل hash‌دار
add_header Cache-Control "no-cache" always;HTML: همیشه بپرس
listen 443 ssl http2;HTTP/2 (Nginx 1.24)