前端開發(fā)必須了解的 Nginx 單頁加載優(yōu)化
從一個前端的角度簡單介紹一下頁面加載的優(yōu)化工作。
網(wǎng)頁加載
首先我們要看一下我們網(wǎng)頁加載到底中間是個什么流程,那些東西比較耗費時間,比如我們訪問github:
- Queued、Queueing:如果是HTTP/1.1的話,會有隊頭阻塞,瀏覽器對每個域名最多開 6 個并發(fā)連接。
- Stalled:瀏覽器要預(yù)先分配資源,調(diào)度連接。
- DNS Lookup:DNS解析域名。
- Initial connection、SSL:與服務(wù)器建立連接,TCP握手,當然你是https的話還有TLS握手。
- Request sent:服務(wù)器發(fā)送數(shù)據(jù)。
- TTFB:等待返回的數(shù)據(jù),網(wǎng)絡(luò)傳輸,也就是首字節(jié)響應(yīng)時間。
- Content Dowload:接收數(shù)據(jù)。
從圖中可以看出從與服務(wù)器建立連接,到接收數(shù)據(jù),這里的時間花費是非常多的,當然還有DNS解析,不過這里有本地緩存,所以基本沒有時間。
gzip-減少加載體積
首先我們可以通過gzip對我們的js以及css進行壓縮:vue.config.js:
- const CompressionWebpackPlugin = require('compression-webpack-plugin')
- buildcfg = {
- productionGzipExtensions: ['js', 'css']
- }
- configureWebpack: (config) => {
- config.plugins.push(
- new CompressionWebpackPlugin({
- test: new RegExp('\\.(' + buildcfg.productionGzipExtensions.join('|') + ')$'),
- threshold: 8192,
- minRatio: 0.8
- })
- )
- }
在nginx里開啟gzip:
server模塊:
- # 使用gzip實時壓縮
- gzip on;
- gzip_min_length 1024;
- gzip_buffers 4 16k;
- gzip_comp_level 6;
- gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript;
- gzip_vary on;
- gzip_disable "MSIE [1-6]\.";
- # 使用gzip_static
- gzip_static on;
- # 使用gzip實時壓縮
- gzip on;
- gzip_min_length 1024;
- gzip_buffers 4 16k;
- gzip_comp_level 6;
- gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript;
- gzip_vary on;
- gzip_disable "MSIE [1-6]\.";
- # 使用gzip_static
- gzip_static on;
這里簡單說明一下吧, + gzip_static是會自動查找對應(yīng)文件的.gz文件,這個的與gzip開啟與否以及gzip_types等并沒有關(guān)聯(lián),你可以理解為優(yōu)先返會.gz文件。
+ gzip的開啟是針對于請求文件的實時壓縮,這個是會消耗cpu的,比如說上面的請求文件的Content-Length大于gzip_min_length,就進行壓縮返回。
總結(jié)一下就是你如果打包后有.gz文件,只需要開啟gzip_static即可,如果沒有那么得啟用gzip實時壓縮,不過我建議使用前者,另外gzip的適用于文本類型,圖片之類的使用的話會適得其反,故gzip_types請適當設(shè)置。
想看gzip是否成功啟用可以通過,查看返回的header頭Content-Encoding: gzip,以及查看文件的size,這里可以看到我們原來文件的是124kb,而返回的gzip文件為44kb,壓縮效率還是蠻大的:
緩存控制-沒有請求就是最好的請求
瀏覽器于服務(wù)器的緩存交互,細說起來可就多了,想了解完整的請看其他人整理的文章吧,我只是這里從配置上略說一下:
- location /mobile {
- alias /usr/share/nginx/html/mobile/;
- try_files uriuri/ /mobile/index.html;
- if (request_filename ~ .*\.(htm|html)){
- add_header Cache-Control no-cache;
- }
- if (request_uri ~* /.*\.(js|css)) {
- # add_header Cache-Control max-age=2592000;
- expires 30d;
- }
- index index.html;
- }
協(xié)商緩存
Last-Modified
我們的單頁入口文件是index.html,這個文件呢決定了我們要加載的js以及css,故我們給html文件設(shè)置協(xié)商緩存Cache-Control no-cache,當我們首次加載時http的狀態(tài)碼為200,服務(wù)器會返回一個Last-Modified表示這個文件的最后修改時間,
再次刷新時瀏覽器會把這個修改時間通過If-Modified-Since發(fā)送給服務(wù)器,如果沒有變動(Etag也會校驗),那么服務(wù)器會返回304狀態(tài)碼,說我的文件沒有變,你直接用緩存吧。
Etag
HTTP協(xié)議解釋Etag是被請求變量的實體標記,你可以理解為一個id,當文件變化了,這個id也會變化,這個和Last-Modified差不多,服務(wù)器會返回一個Etag,瀏覽器下次請求時會帶上If-None-Match,進行對比返回,有些服務(wù)器的Etag計算不同,故在做分布式的時候可能會出問題,文件沒改動不走緩存,當然你可以關(guān)閉這個只使用Last-Modified。
強緩存
我們的單頁應(yīng)用打包時webpack等工具是會根據(jù)文件的變化生成對應(yīng)的js的,也就也是文件不變的話js的hash值不變,故我們在加載js等文件時可以使用強緩存,讓瀏覽器在緩存時間類不進行請求,直接從緩存里面取值,
比如上面我們通過設(shè)置expires(Cache-Control也行,這個優(yōu)先級更高)為30天,那么瀏覽器下此訪問我們相同的緩存過的js和css時(緩存時間內(nèi)),就直接從緩存里面拿(200 from cache),而不會請求我們的服務(wù)器。
注意:此方法是基于上述打包生成hash而言的,假如你生成的是1.js,2.js之類的,那么你修改了1.js里面的類容,打包出來的還是1.js,那么瀏覽器還是會從緩存里面拿,不會進行請求的。也就是說使用此方式需要確保你修改了文件打包后修改的hash值需要變動。
強制刷新
強緩存用得好的話是飛一般的感覺,但是如果在錯誤情況下使用就老是走瀏覽器緩存,如何清理這個呢,我們常用的方式是Ctrl+F5或者在瀏覽器控制臺上把Disable cache給勾上,實際上這個是在請求文件時會自動加上一個header頭Cache-Control: no-cache,也就是說我不要緩存,那么瀏覽器會老老實實的向服務(wù)器發(fā)出請求。
長連接-減少握手次數(shù)
TCP握手以及TLS握手還是比較費時的,比如以前的http1.1之前的連接就是每一條都要進過TCP三次握手,超級費時,還好1.1默認使用了長連接,可以減少握手開銷,但是假如你做大文件上傳時會發(fā)現(xiàn)超過一定時間會斷掉,這是由于Nginx默認的長連接時間為75s,超過了就會斷開,
當你的網(wǎng)頁確實要加載很多很多東西時可以適當把這個時間延長一點,以減少握手次數(shù)(keepalive_requests可以限制keep alive最大請求數(shù)),至于大文件上傳嗎你可以選擇分片上傳,這里就不做介紹了。
server:
- keepalive_timeout 75;
- keepalive_requests 100;
HTTP/2-更安全的 HTTP、更快的 HTTPS
現(xiàn)在很多網(wǎng)站都啟用了HTTP/2,HTTP/2最大的一個優(yōu)點是完全保持了與HTTP/1的兼容,HTTP/2 協(xié)議本身并不要求一定開啟SSL,但瀏覽器要求一定要啟用SSL才能使用HTTP/2,頭部壓縮、虛擬的“流”傳輸、多路復(fù)用等技術(shù)可以充分利用帶寬,降低延遲,從而大幅度提高上網(wǎng)體驗。Nginx開啟相當簡單:
- server {
- listen 443 ssl http2;
- ssl_certificate /etc/nginx/conf.d/ssl/xxx.com.pem;
- ssl_certificate_key /etc/nginx/conf.d/ssl/xxx.com.key;
- ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE+AES128:!MD5:!SHA1; # 棄用不安全的加密套件
- ssl_prefer_server_ciphers on; # 緩解 BEAST 攻擊
- }
HSTS-減少302重定向
現(xiàn)在大多數(shù)網(wǎng)站都是https的了,但是有個問題就是用戶在輸入網(wǎng)址時一般來說不會主動輸入https,走的還是80端口,我們一般會在80端口進行rewrite重寫:
- server{
- listen 80;
- server_name test.com;
- rewrite ^(.*)https://host$1 permanent;
- }
但這種重定向增加了網(wǎng)絡(luò)成本,多出了一次請求,我想下次訪問時直接訪問https怎么處理?我們可以使用HSTS,80端口的不變,在443端口的server新增:
- add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;";
這相當于告訴瀏覽器:我這個網(wǎng)站必須嚴格使用HTTPS協(xié)議,在在max-age時間內(nèi)都不允許用HTTP,下次訪問你就直接用HTTPS吧,那么瀏覽器只會在第一次訪問時走80端口重定向,之后訪問就直接是HTTPS的了(includeSubDomains指定的話那么說明此規(guī)則也適用于該網(wǎng)站的所有子域名)。
Session Ticket-https會話復(fù)用
我們知道https通信時,SSL握手會消耗大量時間,使用非對稱加密保護會話密鑰的生成。而真正傳輸?shù)氖峭ㄟ^對稱加密進行通信傳輸。那么我們每次刷新都進行SSL握手太費時間了,既然雙方都拿到會話密鑰了,那么用這個密鑰進行通信不就可以了,這就是會話復(fù)用。
服務(wù)器把密鑰加密后生成session ticket發(fā)送給客戶端,請求關(guān)閉后,如果客戶端發(fā)起后續(xù)連接(超時時間內(nèi)),下次客戶端再和服務(wù)器建立SSL連接的時候,將此session ticket發(fā)送給服務(wù)器,服務(wù)器解開session ticket后拿出會話密鑰進行加密通信。
- ssl_protocols TLSv1.2 TLSv1.3; # 開啟TLS1.2 以上的協(xié)議
- ssl_session_timeout 5m; # 過期時間,分鐘
- ssl_session_tickets on; # 開啟瀏覽器的Session Ticket緩存