網站性能優化實戰——從12.67s到1.06s的故事
原文作者:IMWeb jerryOnlyZRJ
原文鏈接:網站性能優化實戰--從12.67s到1.06s的故事 - 騰訊Web前端 IMWeb 團隊社區
史上最全面、最完整的網站性能監測與優化策略
0.引言
作為互聯網項目,最重要的便是用戶體驗。在舉國「互聯網+」的熱潮中,用戶至上也已經被大多數企業所接收,特別是在如今移動端快速發展的時代,我們的網頁不僅只是呈現在用戶的PC瀏覽器里,更多的時候,用戶是通過移動產品瀏覽我們的網頁。加之有越來越多的開發者投入到Web APP和Hybrid APP的開發隊伍中,性能,又再一次成為了被程序員們重點關注的話題。我曾經看到過這樣一句話:一個網站的體驗,決定了用戶是否願意去了解網站的功能;而網站的功能,決定了用戶是否會一票否決網站的體驗。這是改版自網路上的一句流行語,但卻把網站性能這件事說的十分透徹,特別是在網站這樣的項目中,如果一個用戶需要超過5s才能看見頁面,他會毫不猶豫地關閉它。
性能優化,作為工程師界的「上乘武功」,是我們在開發中老生常談的話題,也是一名開發者從入門向資深進階的必經階段,雖然我們看到過很多的標準、軍規,但在真正實踐中,卻常常力不從心,不知道落下了什麼,不知道性能是否還有進一步優化的空間。
對於網站的性能,在行業內有很多既定的指標,但就以我們Front-Enders而言,應該更加關注以下指標:白屏時間、首屏時間、整頁時間、DNS時間、CPU佔用率。而我之前自己搭建的一個網站(網址:http://jerryonlyzrj.com/resume/ ,近日因域名備案無法打開,幾日後即恢復正常),完全沒做性能優化時,首屏時間是12.67s,最後經過多方面優化,終於將其降低至1.06s,並且還未配置CDN加速。其中過程我踩了很多坑,也翻了許多專業書籍,最後決定將這幾日的努力整理成文,幫助前端愛好者們少走彎路。
今天,我們將從性能優化的三大方面工作逐步展開介紹,其中包括網路傳輸性能、頁面渲染性能以及JS阻塞性能,系統性地帶著讀者們體驗性能優化的實踐流程。
1.網路傳輸性能優化
在開始介紹網路傳輸性能優化這項工作之前,我們需要了解瀏覽器處理用戶請求的過程,那麼就必須奉上這幅神圖了:
這是navigation timing監測指標圖,從圖中我們可以看出,瀏覽器在得到用戶請求之後,經歷了下面這些階段:重定向→拉取緩存→DNS查詢→建立TCP鏈接→發起請求→接收響應→處理HTML元素→元素載入完成。不著急,我們將對其中的細節一步步展開討論:
1.1.瀏覽器緩存
我們都知道,瀏覽器在向伺服器發起請求前,會先查詢本地是否有相同的文件,如果有,就會直接拉取本地緩存,這和我們在後台部屬的Redis、Memcache類似,都是起到了中間緩衝的作用,我們先看看瀏覽器處理緩存的策略:
因為網上的圖片太籠統了,而且我翻過很多講緩存的文章,很少有將狀態碼還有什麼時候將緩存存放在內存(memory)中什麼時候將緩存在硬碟中(disk)系統地整理出來,所以我自己繪製了一張瀏覽器緩存機制流程圖,結合這張圖再更深入地說明瀏覽器的緩存機制。
這裡我們可以使用chrome devtools里的network面板查看網路傳輸的相關信息:
(這裡需要特別注意,在我們進行緩存調試時,需要去除network面板頂部的Disable cache
勾選項,否則瀏覽器將始終不會從緩存中拉取數據)
瀏覽器默認的緩存是放在內存內的,但我們知道,內存里的緩存會因為進程的結束或者說瀏覽器的關閉而被清除,而存在硬碟里的緩存才能夠被長期保留下去。很多時候,我們在network面板中各請求的size項里,會看到兩種不同的狀態:from memory cache
和 from disk cache
,前者指緩存來自內存,後者指緩存來自硬碟。而控制緩存存放位置的,不是別人,就是我們在伺服器上設置的Etag欄位。在瀏覽器接收到伺服器響應後,會檢測響應頭部(Header),如果有Etag欄位,那麼瀏覽器就會將本次緩存寫入硬碟中。
之所以拉取緩存會出現200、304兩種不同的狀態碼,取決於瀏覽器是否有向伺服器發起驗證請求。 只有向伺服器發起驗證請求並確認緩存未被更新,才會返回304狀態碼。
這裡我以nginx為例,談談如何配置緩存:
首先,我們先進入nginx的配置文檔
$ vim nginxPath/conf/nginx.conf
在配置文檔內插入如下兩項:
etag on; //開啟etag驗證expires 7d; //設置緩存過期時間為7天
打開我們的網站,在chrome devtools的network面板中觀察我們的請求資源,如果在響應頭部看見Etag和Expires欄位,就說明我們的緩存配置成功了。
【!!!特別注意!!!】在我們配置緩存時一定要切記,瀏覽器在處理用戶請求時,如果命中強緩存,瀏覽器會直接拉取本地緩存,不會與伺服器發生任何通信,也就是說,如果我們在伺服器端更新了文件,並不會被瀏覽器得知,就無法替換失效的緩存。所以我們在構建階段,需要為我們的靜態資源添加md5 hash後綴,避免資源更新而引起的前後端文件無法同步的問題。
1.2.資源打包壓縮
我們之前所作的瀏覽器緩存工作,只有在用戶第二次訪問我們的頁面才能起到效果,如果要在用戶首次打開頁面就實現優良的性能,必須對資源進行優化。我們常將網路性能優化措施歸結為三大方面:減少請求數、減小請求資源體積、提升網路傳輸速率。現在,讓我們逐個擊破:
結合前端工程化思想,我們在對上線文件進行自動化打包編譯時,通常都需要打包工具的協助,這裡我推薦webpack,我通常都使用Gulp和Grunt來編譯node,Parcel太新,而且webpack也一直在自身的特性上向Parcel靠攏。
在對webpack進行上線配置時,我們要特別注意以下幾點:
①JS壓縮:(這點應該算是耳熟能詳了,就不多介紹了)
new webpack.optimize.UglifyJsPlugin()
②HTML壓縮:
new HtmlWebpackPlugin({ template: __dirname + /views/index.html, // new 一個這個插件的實例,並傳入相關的參數 filename: ../index.html, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, chunksSortMode: dependency })
我們在使用html-webpack-plugin
自動化注入JS、CSS打包HTML文件時,很少會為其添加配置項,這裡我給出樣例,大家直接複製就行。
PS:這裡有一個技巧,在我們書寫HTML元素的src
或 href
屬性時,可以省略協議部分,這樣也能簡單起到節省資源的目的。
③提取公共資源:
new webpack.optimize.CommonsChunkPlugin({ name: vendor, filename: scripts/common/vendor-[hash:5].js })
PS:這裡是webpack3的語法,在webpack4中已作更改,希望大家注意
④提取css並壓縮:
在使用webpack的過程中,我們通常會以模塊的形式引入css文件(webpack的思想不就是萬物皆模塊嘛),但是在上線的時候,我們還需要將這些css提取出來,並且壓縮,這些看似複雜的過程只需要簡單的幾行配置就行:
(PS:我們需要用到extract-text-webpack-plugin
,所以還得大家自行npm install
)
const ExtractTextPlugin = require(extract-text-webpack-plugin)module: { rules: [..., { test: /.css$/, use: ExtractTextPlugin.extract({ fallback: style-loader, use: { loader: css-loader, options: { minimize: true } } }) }] }
⑤使用webpack3的新特性:ModuleConcatenationPlugin
new webpack.optimize.ModuleConcatenationPlugin()
如果你能按照上述五點將webpack上線配置完整配置出來,基本能將文件資源體積壓縮到極致了,如有疏漏,還希望大家能加以補充。
給大家上一份我的webpack上線配置文檔,歡迎參考:
//webpack.pro.jsconst webpack = require(webpack)const HtmlWebpackPlugin = require(html-webpack-plugin)const ExtractTextPlugin = require(extract-text-webpack-plugin)const CleanWebpackPlugin = require(clean-webpack-plugin)const CopyWebpackPlugin = require(copy-webpack-plugin)module.exports = { entry: __dirname + /public/scripts/index.js, output: { path: __dirname + /build/static, // 打包後的文件存放的地方 filename: scripts/[name]-[hash:5].js // 打包後輸出文件的文件名,帶有md5 hash戳 }, resolve: { extensions: [.jsx, .js] }, module: { rules: [{ test: /(.jsx|.js)$/, use: { loader: babel-loader }, exclude: /node_modules/ // 不進行編譯的目錄 }, { test: /.css$/, use: ExtractTextPlugin.extract({ fallback: style-loader, use: { loader: css-loader, options: { minimize: true } } }) }] }, plugins: [ new HtmlWebpackPlugin({ template: __dirname + /views/index.html, filename: ../index.html, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, chunksSortMode: dependency }), new ExtractTextPlugin(styles/style-[hash:5].css), new CleanWebpackPlugin(build/*, { root: __dirname, verbose: true, dry: false }), new webpack.optimize.UglifyJsPlugin(), new CopyWebpackPlugin([{ from: __dirname + /public/images, to: __dirname + /build/static/images }, { from: __dirname + /public/scripts/vector.js, to: __dirname + /build/static/scripts/vector.js }]), new webpack.optimize.ModuleConcatenationPlugin(), new webpack.optimize.CommonsChunkPlugin({ name: vendor, filename: scripts/common/vendor-[hash:5].js }) ]}
最後,我們還應該在伺服器上開啟Gzip傳輸壓縮,它能將我們的文本類文件體積壓縮至原先的四分之一,效果立竿見影,還是切換到我們的nginx配置文檔,添加如下兩項配置項目:
gzip on;gzip_types text/plain application/javascriptapplication/x-javascripttext/css application/xml text/javascriptapplication/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
如果你在網站請求的響應頭裡看到這樣的欄位,那麼就說明咱們的Gzip壓縮配置成功啦:
【!!!特別注意!!!】不要對圖片文件進行Gzip壓縮!不要對圖片文件進行Gzip壓縮!不要對圖片文件進行Gzip壓縮!我只會告訴你效果適得其反,至於具體原因,還得考慮伺服器壓縮過程中的CPU佔用還有壓縮率等指標,對圖片進行壓縮不但會佔用後台大量資源,壓縮效果其實並不可觀,可以說是「弊大於利」,所以請在gzip_types
把圖片的相關項去掉。針對圖片的相關處理,我們接下來會更加具體地介紹。
1.3.圖片資源優化
剛剛我們介紹了資源打包壓縮,只是停留在了代碼層面,而在我們實際開發中,真正佔用了大量網路傳輸資源的,並不是這些文件,而是圖片,如果你對圖片進行了優化工作,你能立刻看見明顯的效果。
1.3.1.不要在HTML里縮放圖像
很多開發者可能會有這樣的錯覺(其實我曾經也是這樣),比如我們會為了方便在一個200?200的圖片容器內直接使用一張400?400的圖片,我們甚至認為這樣能讓用戶覺得圖片更加清晰,其實不然,在普通的顯示器上,用戶並不會感到縮放後的大圖更加清晰,但這一切卻導致網頁加速速度下降,同時照成帶寬浪費,你可能不知道,一張200KB的圖片和2M的圖片的傳輸時間會是200ms和12s的差距(親身經歷,深受其害(┬_┬))。所以,當你需要用多大的圖片時,就在伺服器上準備好多大的圖片,盡量固定圖片尺寸。
1.3.2.使用雪碧圖(CSS Sprite)
雪碧圖的概念大家一定在開發中經常聽見,其實雪碧圖是減小請求數的示範性代表。而且很奇妙的是,多張圖片拼在一塊後,總體積會比之前所有圖片的體積之和小(你可以親自試試)。這裡給大家推薦一個自動化生成雪碧圖的工具:https://www.toptal.com/developers/css/sprite-generator (圖片來自官網首頁)
只要你添加相關資源文件,他就會自動幫你生成雪碧圖以及對應的CSS樣式,你要做的,只是download和copy。
1.3.3.使用字體圖標(iconfont)
不論是壓縮後的圖片,還是雪碧圖,終歸還是圖片,只要是圖片,就還是會佔用大量網路傳輸資源。但是字體圖標的出現,卻讓前端開發者看到了另外一個神奇的世界。
我最喜歡用的是阿里矢量圖標庫(網址:http://www.iconfont.cn/ ) ,裡面有大量的矢量圖資源,而且你只需要像在淘寶採購一樣把他們添加至購物車就能把它們帶回家,整理完資源後還能自動生成CDN鏈接,可以說是完美的一條龍服務了。(圖片來自官網首頁)
圖片能做的很多事情,矢量圖都能作,而且它只是往HTML里插入字元和CSS樣式而已,和圖片請求比起來,在網路傳輸資源的佔用上它們完全不在一個數量級,如果你的項目里有大量的小圖標,就用矢量圖吧。
1.3.4.使用WebP
WebP格式,是谷歌公司開發的一種旨在加快圖片載入速度的圖片格式。圖片壓縮體積大約只有JPEG的2/3,並能節省大量的伺服器帶寬資源和數據空間。Facebook、Ebay等知名網站已經開始測試並使用WebP格式。
我們可以使用官網提供的Linux命令行工具對項目中的圖片進行WebP編碼,也可以使用我們的線上服務,這裡我推薦叉拍雲(網址:https://www.upyun.com/webp )。但是在實際的上線工作中,我們還是得編寫Shell腳本使用命令行工具進行批量編碼,不過測試階段我們用線上服務就足夠了,方便快捷。(圖片來自叉拍雲官網)
1.4.網路傳輸性能檢測工具——Page Speed
除了network版塊,其實chrome還為我們準備好了一款監測網路傳輸性能的插件——Page Speed,咱們的文章封面,就是用的Page Speed的官方宣傳圖(因為我覺得這張圖再合適不過了)。我們只需要通過下面步驟安裝,就可以在chrome devtools里找到它了:chrome菜單→更多工具→拓展程序→chrome網上應用商店→搜索pagespeed後安轉即可。
(PS:使用chrome應用商店需要翻牆,怎麼翻牆我就不便多說了)
這就是Page Speed的功能界面:
我們只需要打開待測試的網頁,然後點擊Page Speed里的 Start analyzing按鈕,它就會自動幫我們測試網路傳輸性能了,這是我的網站測試結果:
Page Speed最人性化的地方,便是它會對測試網站的性能瓶頸提出完整的建議,我們可以根據它的提示進行優化工作。這裡我的網站已經優化到最好指標了(??????)??,Page Speed Score表示你的性能測試得分,100/100表示已經沒有需要優化的地方。
優化完畢後再使用chorme devtools的network版塊測量一下我們網頁的白屏時間還有首屏時間,是不是得到了很大的提升?
1.5.使用CDN
Last but not least,
再好的性能優化實例,也必須在CDN的支撐下才能到達極致。
如果我們在Linux下使用命令$ traceroute targetIp
或者在Windows下使用批處理 > tracert targetIp
,都可以定位用戶與目標計算機之間經過的所有路由器,不言而喻,用戶和伺服器之間距離越遠,經過的路由器越多,延遲也就越高。使用CDN的目的之一便是解決這一問題,當然不僅僅如此,CDN還可以分擔IDC壓力。
當然,憑著我們單個人的資金實力(除非你是王思聰)是必定搭建不起來CDN的,不過我們可以使用各大企業提供的服務,諸如騰訊雲等,配置也十分簡單,這裡就請大家自行去推敲啦。
其實我們的CDN域名一般是和我們的網站主域名不同的,大家可以看看淘寶、騰訊的官方網站,看看他們存放靜態資源的CDN域名,都是和主域名不一樣的。為什麼要這麼做?主要有兩個原因:[內容摘選自:https://bbs.aliyun.com/simple/t116453.html ]
①便於CDN業務獨立,能夠獨立配置緩存。為了降低web壓力,CDN系統會遵循Cache-Control和Expires HTTP頭標準對改請求返回的內容進行緩存,便於後面的請求不在回源,起到加速功能。而傳統CDN(Web與CDN共用域名)的方式,需要對不同類型的文件設置相應的Cache規則或者遵循後端的HTTP頭,但這樣難以發揮CDN的最大優勢,因為動態請求回源的概率非常之大,如果訪客與源站的線路並不慢,通過CDN的請求未必快於直接請求源站的。 大型網站為了提升web性能到極致,通常緩存頭設置比較大,像谷歌JS設置一年緩存,百度首頁logo設置十年緩存,如果將靜態元素抽取出來,就可以很方便的對所有靜態元素部署規則,而不用考慮動態請求。減少規則的條數可以提升CDN的效率。
②拋開無用cookie,減小帶寬佔用。我們都知道HTTP協議每次發送請求都會自動帶上該域名及父級域名下的cookie,但對於CSS,JS還有圖片資源,這些cookie是沒用的,反而會浪費訪客帶寬和伺服器入帶寬。而我們的主站,為了保持會話或者做其他緩存,都會存放著大量的cookie,所以如果將CDN與主站域名分離,就能解決這一問題。
不過這樣一來,新的問題就出現了:CDN域名與主站域名不同,DNS解析CDN域名還需要花費額外的時間,增加網路延遲。不過這難不住我們偉大的程序員前輩,DNS Prefetch閃亮登場。
如果大家翻看大型網站的HTML源代碼,都會在頭部發現這樣的link鏈接:(這裡以淘寶首頁為例)
這就是DNS Prefetch。DNS Prefetch是一種DNS預解析技術,當我們瀏覽網頁時,瀏覽器會在載入網頁時對網頁中的域名進行預解析並緩存,這樣在瀏覽器載入網頁中的鏈接時,就無需進行DNS解析,減少用戶的等待時間,提高用戶體驗。DNS Prefetch現已被主流瀏覽器支持,大多數瀏覽器針對DNS解析都進行了優化,典型的一次DNS解析會耗費20~120ms,減少DNS解析時間和次數是個很好的優化措施。這裡附上一張Can I use it官網上的DNS Prefetch支持情況圖:
所以,放心大膽地去使用它吧。
2.頁面渲染性能優化
2.1.瀏覽器渲染過程(Webkit)
其實大家應該對瀏覽器的HTML渲染機制比較熟悉了,基本流程同上圖所述,大家在入門的時候,你的導師或者前輩可能會告訴你,在渲染方面我們要減少重排和重繪,因為他們會影響瀏覽器性能。不過你一定不知道其中原理是什麼,對吧。今天我們就結合《Webkit技術內幕》(這本書我還是很推薦大家買來看看,好歹作為一名前端工程師,你得知道我們天天接觸的瀏覽器內核是怎樣工作的)的相關知識,給大家普及普及那些深層次的概念。
PS:這裡提到了Webkit內核,我順帶提一下瀏覽器內部的渲染引擎、解釋器等組件的關係,因為經常有師弟或者一些前端愛好者向我問這方面的知識,分不清他們的關係,我就拿一張圖來說明:(這部分內容與本文無關,如果你對此不感興趣,可以直接跳過)
瀏覽器的解釋器,是包括在渲染引擎內的,我們常說的Chrome(現在使用的是Blink引擎)和Safari使用的Webkit引擎,Firefox使用的Gecko引擎,指的就是渲染引擎。而在渲染引擎內,還包括著我們的HTML解釋器(渲染時用於構造DOM樹)、CSS解釋器(渲染時用於合成CSS規則)還有我們的JS解釋器。不過後來,由於JS的使用越來越重要,工作越來越繁雜,所以JS解釋器也漸漸獨立出來,成為了單獨的JS引擎,就像眾所周知的V8引擎,我們經常接觸的Node.js也是用的它。
2.2.DOM渲染層與GPU硬體加速
如果我告訴你,一個頁面是由許多許多層級組成的,他們就像千層面那樣,你能想像出這個頁面實際的樣子嗎?這裡為了便於大家想像,我附上一張之前Firefox提供的3D View插件的頁面Layers層級圖:
對,你沒看錯,頁面的真實樣子就是這樣,是由多個DOM元素渲染層(Layers)組成的,實際上一個頁面在構建完Render Tree之後,是經歷了這樣的流程才最終呈現在我們面前的:
①瀏覽器會先獲取DOM樹並依據樣式將其分割成多個獨立的渲染層
②CPU將每個層繪製進繪圖中
③將點陣圖作為紋理上傳至GPU(顯卡)繪製
④GPU將所有的渲染層緩存(如果下次上傳的渲染層沒有發生變化,GPU就不需要對其進行重繪)並複合多個渲染層最終形成我們的圖像
從上面的步驟我們可以知道,布局是由CPU處理的,而繪製則是由GPU完成的。
其實在chrome中,也為我們提供了相關插件供我們查看頁面渲染層的分布情況以及GPU的佔用率:(所以說,平時我們得多去嘗試嘗試chrome的那些莫名其妙的插件,真的會發現好多東西都是神器)
chrome開發者工具菜單→more tools→Layers(開啟渲染層功能模塊)
chrome開發者工具菜單→more tools→rendering(開啟渲染性能監測工具)
執行上面的操作後,你會在瀏覽器里看到這樣的效果:
太多東西了,分模塊講吧:
(一)最先是頁面右上方的小黑窗:其實提示已經說的很清楚了,它顯示的就是我們的GPU佔用率,能夠讓我們清楚地知道頁面是否發生了大量的重繪。
(二)Layers版塊:這就是用於顯示我們剛提到的DOM渲染層的工具了,左側的列表裡將會列出頁面里存在哪些渲染層,還有這些渲染層的詳細信息。
(三)Rendering版塊:這個版塊和我們的控制台在同一個地方,大家可別找不到它。前三個勾選項是我們最常使用的,讓我來給大家解釋一下他們的功能(充當一次免費翻譯)
①Paint flashing:勾選之後會對頁面中發生重繪的元素高亮顯示
②Layer borders:和我們的Layer版塊功能類似,它會用高亮邊界突出我們頁面中的各個渲染層
③FPS meter:就是開啟我們在(一)中提到的小黑窗,用於觀察我們的GPU佔用率
可能大家會問我,提到DOM渲染層這麼深的概念有什麼用啊,好像跟性能優化沒一點關係啊?大家應該還記得我剛說到GPU會對我們的渲染層作緩存對吧,那麼大家試想一下,如果我們把那些一直發生大量重排重繪的元素提取出來,單獨觸發一個渲染層,那樣這個元素不就不會「連累」其他元素一塊重繪了對吧。
那麼問題來了,什麼情況下會觸發渲染層呢?大家只要記住:
Video元素、WebGL、Canvas、CSS3 3D、CSS濾鏡、z-index大於某個相鄰節點的元素都會觸發新的Layer,其實我們最常用的方法,就是給某個元素加上下面的樣式:
transform: translateZ(0);backface-visibility: hidden;
這樣就可以觸發渲染層啦 。
我們把容易觸發重排重繪的元素單獨觸發渲染層,讓它與那些「靜態」元素隔離,讓GPU分擔更多的渲染工作,我們通常把這樣的措施成為硬體加速,或者是GPU加速。大家之前肯定聽過這個說法,現在完全清楚它的原理了吧。
2.3.重排與重繪
現在到我們的重頭戲了,重排和重繪。先拋出概念:
①重排(reflow):渲染層內的元素布局發生修改,都會導致頁面重新排列,比如窗口的尺寸發生變化、刪除或添加DOM元素,修改了影響元素盒子大小的CSS屬性(諸如:width、height、padding)。
②重繪(repaint):繪製,即渲染上色,所有對元素的視覺表現屬性的修改,都會引發重繪。
我們習慣使用chrome devtools中的performance版塊來測量頁面重排重繪所佔據的時間:
①藍色部分:HTML解析和網路通信佔用的時間
②黃色部分:JavaScript語句執行所佔用時間
③紫色部分:重排佔用時間
④綠色部分:重繪佔用時間
不論是重排還是重繪,都會阻塞瀏覽器。要提高網頁性能,就要降低重排和重繪的頻率和成本,近可能少地觸發重新渲染。正如我們在2.3中提到的,重排是由CPU處理的,而重繪是由GPU處理的,CPU的處理效率遠不及GPU,並且重排一定會引發重繪,而重繪不一定會引發重排。所以在性能優化工作中,我們更應當著重減少重排的發生。
這裡給大家推薦一個網站,裡面詳細列出了哪些CSS屬性在不同的渲染引擎中是否會觸發重排或重繪:
https://csstriggers.com/ (圖片來自官網)
2.4.優化策略
談了那麼多理論,最實際不過的,就是解決方案,大家一定都等著急了吧,做好準備,一大波乾貨來襲:
(一)CSS屬性讀寫分離:瀏覽器每次對元素樣式進行讀操作時,都必須進行一次重新渲染(重排 + 重繪),所以我們在使用JS對元素樣式進行讀寫操作時,最好將兩者分離開,先讀後寫,避免出現兩者交叉使用的情況。最最最客觀的解決方案,就是不用JS去操作元素樣式,這也是我最推薦的。
(二)通過切換class或者使用元素的style.csstext屬性去批量操作元素樣式。
(三)DOM元素離線更新:當對DOM進行相關操作時,例、appendChild等都可以使用Document Fragment對象進行離線操作,帶元素「組裝」完成後再一次插入頁面,或者使用display:none
對元素隱藏,在元素「消失」後進行相關操作。
(四)將沒用的元素設為不可見:visibility: hidden
,這樣可以減小重繪的壓力,必要的時候再將元素顯示。
(五)壓縮DOM的深度,一個渲染層內不要有過深的子元素,少用DOM完成頁面樣式,多使用偽元素或者box-shadow取代。
(六)圖片在渲染前指定大小:因為img元素是內聯元素,所以在載入圖片後會改變寬高,嚴重的情況會導致整個頁面重排,所以最好在渲染前就指定其大小,或者讓其脫離文檔流。
(七)對頁面中可能發生大量重排重繪的元素單獨觸發渲染層,使用GPU分擔CPU壓力。(這項策略需要慎用,得著重考量以犧牲GPU佔用率為代價能否換來可期的性能優化,畢竟頁面中存在太多的渲染層對於GPU而言也是一種不必要的壓力,通常情況下,我們會對動畫元素採取硬體加速。)
3.JS阻塞性能
JavaScript在網站開發中幾乎已經確定了壟斷地位,哪怕是一個再簡單不過的靜態頁面,你都可能看到JS的存在,可以說,沒有JS,網站就基本告別用戶交互了。然而,腳本帶來的問題就是他會阻塞頁面的平行下載,還會提高進程的CPU佔用率。更有甚者,現在node.js已經在前端開發中普及,稍有不慎,我們引發了內存泄漏,或者在代碼中誤寫了死循環,會直接造成我們的伺服器崩潰。在如今這個JS已經遍布前後端的時代,性能的瓶頸不單單只是停留在影響用戶體驗上,還會有更多更為嚴重的問題,對JS的性能優化工作不可小覷。
在編程的過程中,如果我們使用了閉包後未將相關資源加以釋放,或者引用了外鏈後未將其置空(比如給某DOM元素綁定了事件回調,後來卻remove了該元素),都會造成內存泄漏的情況發生,進而大量佔用用戶的CPU,造成卡頓或死機。我們可以使用chrome提供的JavaScript Profile版塊,開啟方式同Layers等版塊,這裡我就不再多說了,直接上效果圖:
我們可以清楚看見JS執行時各函數的執行時間以及CPU佔用情況,如果我在代碼里增加一行while(true){}
, 那麼它的佔用率一定會飆升到一個異常的指標(親測93.26%)。
其實瀏覽器強大的內存回收機制在大多數時候避免了這一情況的發生,即便用戶發生了死機,他只要結束相關進程(或關閉瀏覽器)就可以解決這一問題,但我們要知道,同樣的情況還會發生在我們的伺服器端,也就是我們的node中,嚴重的情況,會直接造成我們的伺服器宕機,網站崩潰。所以更多時候,我們都使用JavaScript Profile版塊來對我們的node服務進行壓力測試,搭配node-inspector
插件,我們能更有效地檢測JS執行時各函數的CPU佔用率,針對性地進行優化。
(PS:所以沒修鍊到一定水平,千萬別在服務端使用閉包,一個是真沒啥用,我們會有更多優良的解決辦法,二是真的很容易內存泄漏,造成的後果是你無法預期的)
4.【拓展】負載均衡
之所以將負載均衡作為拓展內容,是因為如果是你自己搭建的個人網站,或者中小型網站,其實並不需要考慮多大的並發量,但是如果你搭建的是大型網站,負載均衡便是開發過程不可或缺的步驟。
4.1.Node.js處理IO密集型請求
現在的開發流程都注重前後端分離,也就是軟體工程中常提到的「高內聚低耦合」的思想,你也可以用模塊化的思想去理解,前後解耦就相當與把一個項目分成了前端和後端兩個大模塊,中間通過介面聯繫起來,分別進行開發。這樣做有什麼好處?我就舉最有實際效果的一點:「非同步編程」。這是我自己想的名字,因為我覺得前後解耦的形式很像我們JS中的非同步隊列,傳統的開發模式是「同步」的,前端需要等後端封裝好介面,知道了能拿什麼數據,再去開發,時間短,工程大。而解耦之後,我們只需要提前約定好介面,前後兩端就可以同時開發,不僅高效而且省時。
我們都知道node的核心是事件驅動,通過Event Loop去非同步處理用戶請求,相比於傳統的後端服務,它們都是將用戶的每個請求分配一個進程進行處理,推薦大家去看這樣一篇博文:https://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513044&idx=1&sn=9b8526e9d641b970ee5ddac02dae3c57&scene=21#wechat_redirect 。特別生動地講解了事件驅動的運行機制,通俗易懂。事件驅動的最大優勢是什麼?就是在高並發IO時,不會造成堵塞,對於直播類網站,這點是至關重要的,我們有成功的先例——快手,快手強大的IO高並發究其本質一定能追溯到node。
其實現在的企業級網站,都會搭建一層node作為中間層。大概的網站框架如圖所示:
4.2.pm2實現Node.js「多進程」
我們都知道node的優劣,這裡分享一份鏈接,找了挺久寫的還算詳細:https://www.zhihu.com/question/19653241/answer/15993549 。其實很多都是老套路,那些說node不行的都是指著node是單進程這一個軟肋開撕,告訴你,我們有解決方案了——pm2。這是它的官網:http://pm2.keymetrics.io/ 。它是一款node.js進程管理器,具體的功能,就是能在你的計算機里的每一個內核都啟動一個node.js服務,也就是說如果你的電腦或者伺服器是多核處理器(現在也少見單核了吧),它就能啟動多個node.js服務,並且它能夠自動控制負載均衡,會自動將用戶的請求分發至壓力小的服務進程上處理。聽起來這東西簡直就是神器啊!而且它的功能遠遠不止這些,這裡我就不作過多介紹了,大家知道我們在上線的時候需要用到它就行了,安裝的方法也很簡單,直接用npm下到全局就可以了$ npm i pm2 -g
具體的使用方法還有相關特性可以參照官網。
下面是pm2啟動後的效果圖:
4.3.nginx搭建反向代理
在開始搭建工作之前,首先得知道什麼是反向代理。可能大家對這個名詞比較陌生,先上一張圖:
所謂代理就是我們通常所說的中介,網站的反向代理就是指那台介於用戶和我們真實伺服器之間的伺服器(說的我都拗口了),它的作用便是能夠將用戶的請求分配到壓力較小的伺服器上,其機制是輪詢。聽完這句話是不是感覺很耳熟,沒錯,在我介紹pm2的時候也說過同樣的話,反向代理起到的作用同pm2一樣也是實現負載均衡,你現在應該也明白了兩者之間的差異,反向代理是對伺服器實現負載均衡,而pm2是對進程實現負載均衡。大家如果想深入了解反向代理的相關知識,我推薦知乎的一個貼子:https://www.zhihu.com/question/24723688 。但是大家會想到,配伺服器是運維的事情啊,和我們前端有什麼關係呢?的確,在這部分,我們的工作只有一些,只需要向運維提供一份配置文檔即可。
http { upstream video { ip_hash; server localhost:3000; } server { listen: 8080; location / { proxy_pass: http://video } }}
也就是說,在和運維對接的時候,我們只需要將上面這幾行代碼改為我們配置好的文檔發送給他就行了,其他的事情,運維小哥會明白的,不用多說,都在酒里。
但是,這幾行代碼該怎麼去改呢?首先我們得知道,在nginx中,模塊被分為三大類:handler、filter和upstream。而其中的upstream模塊,負責完成完成網路數據的接收、處理和轉發,也是我們需要在反向代理中用到的模塊。接下來我們將介紹配置代碼里的內容所表示的含義:
4.3.1.upstream配置信息
upstream關鍵字後緊跟的標識符是我們自定義的項目名稱,通過一對花括弧在其中增添我們的配置信息。
ip_hash
關鍵字:控制用戶再次訪問時是否連接到前一次連接的伺服器
server
關鍵字:我們真實伺服器的地址,這裡的內容肯定是需要我們去填寫的,不然運維怎麼知道你把項目放在那個伺服器上了,也不知道你封裝了一層node而得去監聽3000埠。
4.3.2.server配置信息
server是nginx的基本配置,我們需要通過server將我們定義的upstream應用到伺服器上。
listen
關鍵字:伺服器監聽的埠
location
關鍵字:和我們之前在node層說到的路由是起同樣的功能,這裡是把用戶的請求分配到對應的upstream上
5.拓展閱讀
網站的性能與監測是一項複雜的工程,還有很多很多後續的工作,我之前所提到的這些,也只能算是冰山一角,在熟悉開發規範的同時,也需要實踐經驗的積累。
在翻閱了許多與網站性能相關的書籍後,我還是更鐘情於唐文前輩編著的《大型網站性能監測、分析與優化》,裡面的知識較新,切合實際,至少我讀完一遍後很有收穫、醍醐灌頂,我也希望對性能感興趣的讀者在看完我的文章後能去翻翻這本著作。
前端NEXT學位課程第六期正在招生中!感興趣的小夥伴快點擊這裡,了解課程詳情吧!
更多乾貨與福利請關注公眾號【騰訊NEXT學位】!
推薦閱讀:
※小記JS模塊化
※Vue.js前後端同構方案之準備篇——代碼優化
※node.js-腳本合併
※[譯] HTTP/2 Server Push 詳解