性能優化是把雙刃劍,有好的一面也有壞的一面。好的一面就是能提高網站性能,壞的一面就是配置麻煩,或者要遵照的規則太多。而且某些性能優化規則並不適用全部場景,須要謹慎使用,請讀者帶着批判性的眼光來閱讀本文。html
本文相關的優化建議的引用資料出處均會在建議後面給出,或者放在文末。前端
一個完整的 HTTP 請求須要經歷 DNS 查找,TCP 握手,瀏覽器發出 HTTP 請求,服務器接收請求,服務器處理請求併發迴響應,瀏覽器接收響應等過程。接下來看一個具體的例子幫助理解 HTTP :vue
這是一個 HTTP 請求,請求的文件大小爲 28.4KB。node
名詞解釋:webpack
從這個例子能夠看出,真正下載數據的時間佔比爲 13.05 / 204.16 = 6.39%
,文件越小,這個比例越小,文件越大,比例就越高。這就是爲何要建議將多個小文件合併爲一個大文件,從而減小 HTTP 請求次數的緣由。git
參考資料:github
HTTP2 相比 HTTP1.1 有以下幾個優勢:web
服務器解析 HTTP1.1 的請求時,必須不斷地讀入字節,直到遇到分隔符 CRLF 爲止。而解析 HTTP2 的請求就不用這麼麻煩,由於 HTTP2 是基於幀的協議,每一個幀都有表示幀長度的字段。面試
HTTP1.1 若是要同時發起多個請求,就得創建多個 TCP 鏈接,由於一個 TCP 鏈接同時只能處理一個 HTTP1.1 的請求。算法
在 HTTP2 上,多個請求能夠共用一個 TCP 鏈接,這稱爲多路複用。同一個請求和響應用一個流來表示,並有惟一的流 ID 來標識。 多個請求和響應在 TCP 鏈接中能夠亂序發送,到達目的地後再經過流 ID 從新組建。
HTTP2 提供了首部壓縮功能。
例若有以下兩個請求:
:authority: unpkg.zhimg.com :method: GET :path: /za-js-sdk@2.16.0/dist/zap.js :scheme: https accept: */* accept-encoding: gzip, deflate, br accept-language: zh-CN,zh;q=0.9 cache-control: no-cache pragma: no-cache referer: https://www.zhihu.com/ sec-fetch-dest: script sec-fetch-mode: no-cors sec-fetch-site: cross-site user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
:authority: zz.bdstatic.com :method: GET :path: /linksubmit/push.js :scheme: https accept: */* accept-encoding: gzip, deflate, br accept-language: zh-CN,zh;q=0.9 cache-control: no-cache pragma: no-cache referer: https://www.zhihu.com/ sec-fetch-dest: script sec-fetch-mode: no-cors sec-fetch-site: cross-site user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
從上面兩個請求能夠看出來,有不少數據都是重複的。若是能夠把相同的首部存儲起來,僅發送它們之間不一樣的部分,就能夠節省很多的流量,加快請求的時間。
HTTP/2 在客戶端和服務器端使用「首部表」來跟蹤和存儲以前發送的鍵-值對,對於相同的數據,再也不經過每次請求和響應發送。
下面再來看一個簡化的例子,假設客戶端按順序發送以下請求首部:
Header1:foo Header2:bar Header3:bat
當客戶端發送請求時,它會根據首部值建立一張表:
索引 | 首部名稱 | 值 |
---|---|---|
62 | Header1 | foo |
63 | Header2 | bar |
64 | Header3 | bat |
若是服務器收到了請求,它會照樣建立一張表。 當客戶端發送下一個請求的時候,若是首部相同,它能夠直接發送這樣的首部塊:
62 63 64
服務器會查找先前創建的表格,並把這些數字還原成索引對應的完整首部。
HTTP2 能夠對比較緊急的請求設置一個較高的優先級,服務器在收到這樣的請求後,能夠優先處理。
因爲一個 TCP 鏈接流量帶寬(根據客戶端到服務器的網絡帶寬而定)是固定的,當有多個請求併發時,一個請求佔的流量多,另外一個請求佔的流量就會少。流量控制能夠對不一樣的流的流量進行精確控制。
HTTP2 新增的一個強大的新功能,就是服務器能夠對一個客戶端請求發送多個響應。換句話說,除了對最初請求的響應外,服務器還能夠額外向客戶端推送資源,而無需客戶端明確地請求。
例如當瀏覽器請求一個網站時,除了返回 HTML 頁面外,服務器還能夠根據 HTML 頁面中的資源的 URL,來提早推送資源。
如今有不少網站已經開始使用 HTTP2 了,例如知乎:
其中 h2 是指 HTTP2 協議,http/1.1 則是指 HTTP1.1 協議。
參考資料:
客戶端渲染: 獲取 HTML 文件,根據須要下載 JavaScript 文件,運行文件,生成 DOM,再渲染。
服務端渲染:服務端返回 HTML 文件,客戶端只需解析 HTML。
參考資料:
內容分發網絡(CDN)是一組分佈在多個不一樣地理位置的 Web 服務器。咱們都知道,當服務器離用戶越遠時,延遲越高。CDN 就是爲了解決這一問題,在多個位置部署服務器,讓用戶離服務器更近,從而縮短請求時間。
當用戶訪問一個網站時,若是沒有 CDN,過程是這樣的:
若是用戶訪問的網站部署了 CDN,過程是這樣的:
參考資料:
全部放在 head 標籤裏的 CSS 和 JS 文件都會堵塞渲染。若是這些 CSS 和 JS 須要加載和解析好久的話,那麼頁面就空白了。因此 JS 文件要放在底部,等 HTML 解析完了再加載 JS 文件。
那爲何 CSS 文件還要放在頭部呢?
由於先加載 HTML 再加載 CSS,會讓用戶第一時間看到的頁面是沒有樣式的、「醜陋」的,爲了不這種狀況發生,就要將 CSS 文件放在頭部了。
另外,JS 文件也不是不能夠放在頭部,只要給 script 標籤加上 defer 屬性就能夠了,異步下載,延遲執行。
字體圖標就是將圖標製做成一個字體,使用時就跟字體同樣,能夠設置屬性,例如 font-size、color 等等,很是方便。而且字體圖標是矢量圖,不會失真。還有一個優勢是生成的文件特別小。
使用 fontmin-webpack 插件對字體文件進行壓縮(感謝前端小偉提供)。
參考資料:
爲了不用戶每次訪問網站都得請求文件,咱們能夠經過添加 Expires 或 max-age 來控制這一行爲。Expires 設置了一個時間,只要在這個時間以前,瀏覽器都不會請求文件,而是直接使用緩存。而 max-age 是一個相對時間,建議使用 max-age 代替 Expires 。
不過這樣會產生一個問題,當文件更新了怎麼辦?怎麼通知瀏覽器從新請求文件?
能夠經過更新頁面中引用的資源連接地址,讓瀏覽器主動放棄緩存,加載新資源。
具體作法是把資源地址 URL 的修改與文件內容關聯起來,也就是說,只有文件內容變化,纔會致使相應 URL 的變動,從而實現文件級別的精確緩存控制。什麼東西與文件內容相關呢?咱們會很天然的聯想到利用數據摘要要算法對文件求摘要信息,摘要信息與文件內容一一對應,就有了一種能夠精確到單個文件粒度的緩存控制依據了。
參考資料:
壓縮文件能夠減小文件下載時間,讓用戶體驗性更好。
得益於 webpack 和 node 的發展,如今壓縮文件已經很是方便了。
在 webpack 可使用以下插件進行壓縮:
其實,咱們還能夠作得更好。那就是使用 gzip 壓縮。能夠經過向 HTTP 請求頭中的 Accept-Encoding 頭添加 gzip 標識來開啓這一功能。固然,服務器也得支持這一功能。
gzip 是目前最流行和最有效的壓縮方法。舉個例子,我用 Vue 開發的項目構建後生成的 app.js 文件大小爲 1.4MB,使用 gzip 壓縮後只有 573KB,體積減小了將近 60%。
附上 webpack 和 node 配置 gzip 的使用方法。
下載插件
npm install compression-webpack-plugin --save-dev npm install compression
webpack 配置
const CompressionPlugin = require('compression-webpack-plugin'); module.exports = { plugins: [new CompressionPlugin()], }
node 配置
const compression = require('compression') // 在其餘中間件前使用 app.use(compression())
在頁面中,先不給圖片設置路徑,只有當圖片出如今瀏覽器的可視區域時,纔去加載真正的圖片,這就是延遲加載。對於圖片不少的網站來講,一次性加載所有圖片,會對用戶體驗形成很大的影響,因此須要使用圖片延遲加載。
首先能夠將圖片這樣設置,在頁面不可見時圖片不會加載:
<img data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4">
等頁面可見時,使用 JS 加載圖片:
const img = document.querySelector('img') img.src = img.dataset.src
這樣圖片就加載出來了,完整的代碼能夠看一下參考資料。
參考資料:
響應式圖片的優勢是瀏覽器可以根據屏幕大小自動加載合適的圖片。
經過 picture
實現
<picture> <source srcset="banner_w1000.jpg" media="(min-width: 801px)"> <source srcset="banner_w800.jpg" media="(max-width: 800px)"> <img src="banner_w800.jpg" alt=""> </picture>
經過 @media
實現
@media (min-width: 769px) { .bg { background-image: url(bg1080.jpg); } } @media (max-width: 768px) { .bg { background-image: url(bg768.jpg); } }
例如,你有一個 1920 * 1080 大小的圖片,用縮略圖的方式展現給用戶,而且當用戶鼠標懸停在上面時才展現全圖。若是用戶從未真正將鼠標懸停在縮略圖上,則浪費了下載圖片的時間。
因此,咱們能夠用兩張圖片來實行優化。一開始,只加載縮略圖,當用戶懸停在圖片上時,才加載大圖。還有一種辦法,即對大圖進行延遲加載,在全部元素都加載完成後手動更改大圖的 src 進行下載。
例如 JPG 格式的圖片,100% 的質量和 90% 質量的一般看不出來區別,尤爲是用來當背景圖的時候。我常常用 PS 切背景圖時, 將圖片切成 JPG 格式,而且將它壓縮到 60% 的質量,基本上看不出來區別。
壓縮方法有兩種,一是經過 webpack 插件 image-webpack-loader
,二是經過在線網站進行壓縮。
如下附上 webpack 插件 image-webpack-loader
的用法。
npm i -D image-webpack-loader
webpack 配置
{ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use:[ { loader: 'url-loader', options: { limit: 10000, /* 圖片大小小於1000字節限制時會自動轉成 base64 碼引用*/ name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, /*對圖片進行壓縮*/ { loader: 'image-webpack-loader', options: { bypassOnDebug: true, } } ] }
有不少圖片使用 CSS 效果(漸變、陰影等)就能畫出來,這種狀況選擇 CSS3 效果更好。由於代碼大小一般是圖片大小的幾分之一甚至幾十分之一。
參考資料:
WebP 的優點體如今它具備更優的圖像數據壓縮算法,能帶來更小的圖片體積,並且擁有肉眼識別無差別的圖像質量;同時具有了無損和有損的壓縮模式、Alpha 透明以及動畫的特性,在 JPEG 和 PNG 上的轉化效果都至關優秀、穩定和統一。
參考資料:
懶加載或者按需加載,是一種很好的優化網頁或應用的方式。這種方式其實是先把你的代碼在一些邏輯斷點處分離開,而後在一些代碼塊中完成某些操做後,當即引用或即將引用另一些新的代碼塊。這樣加快了應用的初始加載速度,減輕了它的整體體積,由於某些代碼塊可能永遠不會被加載。
經過配置 output 的 filename 屬性能夠實現這個需求。filename 屬性的值選項中有一個 [contenthash],它將根據文件內容建立出惟一 hash。當文件內容發生變化時,[contenthash] 也會發生變化。
output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', path: path.resolve(__dirname, '../dist'), },
因爲引入的第三方庫通常都比較穩定,不會常常改變。因此將它們單獨提取出來,做爲長期緩存是一個更好的選擇。 這裏須要使用 webpack4 的 splitChunk 插件 cacheGroups 選項。
optimization: { runtimeChunk: { name: 'manifest' // 將 webpack 的 runtime 代碼拆分爲一個單獨的 chunk。 }, splitChunks: { cacheGroups: { vendor: { name: 'chunk-vendors', test: /[\\/]node_modules[\\/]/, priority: -10, chunks: 'initial' }, common: { name: 'chunk-common', minChunks: 2, priority: -20, chunks: 'initial', reuseExistingChunk: true } }, } },
Babel 轉化後的代碼想要實現和原來代碼同樣的功能須要藉助一些幫助函數,好比:
class Person {}
會被轉換爲:
"use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Person = function Person() { _classCallCheck(this, Person); };
這裏 _classCallCheck
就是一個 helper
函數,若是在不少文件裏都聲明瞭類,那麼就會產生不少個這樣的 helper
函數。
這裏的 @babel/runtime
包就聲明瞭全部須要用到的幫助函數,而 @babel/plugin-transform-runtime
的做用就是將全部須要 helper
函數的文件,從 @babel/runtime包
引進來:
"use strict"; var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck"); var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var Person = function Person() { (0, _classCallCheck3.default)(this, Person); };
這裏就沒有再編譯出 helper
函數 classCallCheck
了,而是直接引用了 @babel/runtime
中的 helpers/classCallCheck
。
安裝
npm i -D @babel/plugin-transform-runtime @babel/runtime
使用 在 .babelrc
文件中
"plugins": [ "@babel/plugin-transform-runtime" ]
參考資料:
瀏覽器渲染過程
重排
當改變 DOM 元素位置或大小時,會致使瀏覽器從新生成渲染樹,這個過程叫重排。
重繪
當從新生成渲染樹後,就要將渲染樹每一個節點繪製到屏幕,這個過程叫重繪。不是全部的動做都會致使重排,例如改變字體顏色,只會致使重繪。記住,重排會致使重繪,重繪不會致使重排 。
重排和重繪這兩個操做都是很是昂貴的,由於 JavaScript 引擎線程與 GUI 渲染線程是互斥,它們同時只能一個在工做。
什麼操做會致使重排?
如何減小重排重繪?
事件委託利用了事件冒泡,只指定一個事件處理程序,就能夠管理某一類型的全部事件。全部用到按鈕的事件(多數鼠標事件和鍵盤事件)都適合採用事件委託技術, 使用事件委託能夠節省內存。
<ul> <li>蘋果</li> <li>香蕉</li> <li>鳳梨</li> </ul> // good document.querySelector('ul').onclick = (event) => { const target = event.target if (target.nodeName === 'LI') { console.log(target.innerHTML) } } // bad document.querySelectorAll('li').forEach((e) => { e.onclick = function() { console.log(this.innerHTML) } })
同時,我還從這位阿里大神手裏薅到一份阿里內部資料。
篇幅有限,僅展現部份內容
但願你們明年的金三銀四面試順利,拿下本身心儀的offer!