توی این درس یاد میگیری با دو ابزار ساده، سایت را برای کاربرانت چند برابر سریعتر کنی: فشردهسازی (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 “چه چیزی را فشرده و چه چیزی را کش کنیم؟”مثالهای عملی
Section titled “مثالهای عملی”این درس روی ماشین آزمایشی 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”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" نقش مرورگر را بازی میکنیم:
ln -s /etc/nginx/sites-available/loopx.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1cd /var/www/loopx.testcss=/_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.pathfor 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)'donecd ~nginx: configuration file /etc/nginx/nginx.conf test is successful== /docker/what-is-docker/Content-Type: text/htmlContent-Encoding: gzip== /_astro/common.D0VTP8CO.cssContent-Type: text/cssContent-Length: 84442== /_astro/ui-core.hRq-JN9-.jsContent-Type: application/javascriptContent-Length: 93823== /favicon.svgContent-Type: image/svg+xmlContent-Length: 10806HTML با Content-Encoding: gzip آمد (اندازهاش در هدر نیست چون در حین ارسال فشرده میشود و اندازهی نهایی از قبل معلوم نیست). ولی CSS، JS و SVG بدون Content-Encoding و با اندازهی کامل (Content-Length) فرستاده شدند. چرا؟
grep -n 'gzip' /etc/nginx/nginx.conf46: 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، برای همهی سایتها) میگذاریم:
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;nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1css=$(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 ))"donenginx: configuration file /etc/nginx/nginx.conf test is successfulfile 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 از آن میگیریم:
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;}EOFnginx -t && systemctl reload nginx && sleep 1css=$(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"donenginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successfulidentity: 84442 bytes in 7.235594sgzip: 17971 bytes in 1.004197slimit_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/) و باید هر بار پرسیده شود:
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;}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1css=$(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)'donenginx: configuration file /etc/nginx/nginx.conf test is successful== /_astro/common.D0VTP8CO.cssExpires: Mon, 04 Oct 2027 14:49:07 GMTCache-Control: max-age=31536000Cache-Control: public, immutable== /og/docker.pngExpires: Sun, 11 Oct 2026 14:49:07 GMTCache-Control: max-age=604800== /docker/what-is-docker/Cache-Control: no-cacheexpires 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 بود، همان را مستقیم میفرستد:
cd /var/www/loopx.test/_astrocss=$(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.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1echo "=== بعد از 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.css17500 common.D0VTP8CO.css.gz=== قبل از gzip_static (فشردهسازی در لحظه، سطح ۵):17971 bytesnginx: configuration file /etc/nginx/nginx.conf test is successful=== بعد از gzip_static (فایل .gz سطح ۹):17500 bytesContent-Length: 17500Content-Encoding: gzipاندازهی پاسخ دقیقاً اندازهی فایل .gz شد؛ یعنی Nginx خودش فشرده نکرد و فایل آماده را فرستاد (و چون اندازه از قبل معلوم است، Content-Length هم دارد). مزیت دوبرابر: CPU صفر برای فشردهسازی، و فشردهسازی سطح ۹ بدون هزینهی لحظهای. خیلی از ابزارهای build (و پلاگینهای Vite و Astro) میتوانند فایلهای .gz (و .br برای Brotli) را خودشان بسازند.
مثال ۶: HTTP/2
Section titled “مثال ۶: HTTP/2”در HTTP/1.1، مرورگر برای هر فایل یک درخواست جدا میفرستد و تعداد اتصالهای همزمان محدود است. HTTP/2 همهی فایلها را روی یک اتصال و همزمان میفرستد و هدرها را هم فشرده میکند. در Nginx فقط روی HTTPS (عملاً، چون مرورگرها HTTP/2 را فقط روی TLS پشتیبانی میکنند) و با یک کلمه:
mkdir -p /etc/nginx/sslopenssl 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/nullchmod 600 /etc/nginx/ssl/loopx.keysed -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.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s -I --cacert /etc/nginx/ssl/loopx.crt https://loopx.test/ | head -n 1curl -s -I --http1.1 --cacert /etc/nginx/ssl/loopx.crt https://loopx.test/ | head -n 1nginx: configuration file /etc/nginx/nginx.conf test is successfulHTTP/2 200HTTP/1.1 200 OKcurl خودش 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 را با چند سطح (از ۱ تا ۹) فشرده میکنیم و زمان را با ۲۰۰ بار تکرار میسنجیم:
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"doneoriginal: 84442 byteslevel bytes ms/200x1 21656 5883 20051 5965 17900 6736 17614 7479 17500 927از سطح ۱ تا ۵ اندازه بهوضوح کم میشود؛ بعد از آن سود اندازه خیلی کم است ولی زمان CPU بیشتر میشود (سطح ۹ چند برابر سطح ۱). برای فشردهسازی در لحظه ۴ تا ۶ منطقی است؛ سطح ۹ را برای gzip_static (یک بار، در build) نگه دار. (این زمانها برای ابزار gzip خط فرمان است؛ Nginx از همان کتابخانهی zlib استفاده میکند و الگوی هزینه یکی است.)
جدولهای مرجع
Section titled “جدولهای مرجع”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 |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) اعتماد به 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.
۳) کش طولانی برای HTML
Section titled “۳) کش طولانی برای HTML”cat > /etc/nginx/conf.d/lx-badcache.conf <<'EOF'server { listen 8096; root /var/www/loopx.test; location / { expires 30d; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s -I http://localhost:8096/ | grep -iE '^cache-control'rm /etc/nginx/conf.d/lx-badcache.confsystemctl reload nginxnginx: configuration file /etc/nginx/nginx.conf test is successfulCache-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 صفحهی اصلی دربیاور.)
دیدن جواب
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=0for 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))doneecho "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)"donefiles: 6without gzip: 189950 byteswith 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 میگیرد.
دیدن جواب
ok=0; bad=0for 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"; fidoneecho "_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 میآید.
دیدن جواب
mkdir -p /srv/lx-apicat > /srv/lx-api/api.py <<'EOF'# api.py: big JSON at /plain, already-gzipped JSON at /pregzimport gzip, jsonfrom http.server import BaseHTTPRequestHandler, HTTPServerDATA = 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): passHTTPServer(("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.testsleep 1nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for 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"))"donepkill -f '/srv/lx-api/api.py'nginx: configuration file /etc/nginx/nginx.conf test is successful== /api/plainVary: Accept-EncodingContent-Encoding: gzip size: 3750 bytes (plain: 26667)== /api/pregzContent-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 بسپارد.)
آزمونک
Section titled “آزمونک”در Ubuntu، gzip on روشن است ولی CSS فشرده نمیشود. چرا؟
gzip_types را صریح بنویس (text/css application/javascript ...).
چرا فایل /_astro/common.D0VTP8CO.css را میشود یک سال کش کرد ولی /index.html را نه؟
HTML: no-cache؛ hashدار: max-age=1y, immutable.
Cache-Control: no-cache یعنی چه؟
برای «هرگز ذخیره نکن» no-store است.
gzip_comp_level 9 برای فشردهسازی در لحظه چه ایرادی دارد؟
سطح ۴ تا ۶ تعادل خوبی است.
gzip_static on چه میکند؟
CPU صفر و فشردهسازی حداکثری.
gzip_vary on چه هدری اضافه میکند و چرا مهم است؟
پاسخ یک آدرس به Accept-Encoding درخواست بستگی دارد.
چرا image/png را در gzip_types نمیگذاریم؟
فقط فرمتهای متنی (و SVG).
جمعبندی
Section titled “جمعبندی”- 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) |