前端性能優化總結

性能爲什麼相當重要

引自:性能爲什麼相當重要javascript

網站開始加載時,用戶須要等待一段時間才能看到要顯示的內容。這部分時間是能夠經過性能優化控制的。css

  • 性能關乎用戶的去留

當用戶等待的時間超過忍耐的程度時,必將拋棄該應用html

  • 性能關乎轉化率的提高

響應速度慢會對網站收入帶來不利影響,反之亦然。前端

  • 性能關乎用戶體驗

等待頁面的加載時間就是在考驗用戶的忍耐力,而用戶的忍受時間也是有一個閾值的。vue

  • 性能關乎用戶

性能低的網站和應用還會致使用戶產生實際成本。java

http 緩存

大部分性能優化都是基於緩存用空間換時間,通常使用資源的緩存提升加載的性能。node

Expires

設置緩存對象的有效期jquery

由於工程化的發展,目前不多有人用這個參數去控制 http 請求的緩存了,用過的人可能知道,這個參數如今最多見的就是在設置 cookie 上webpack

若是有人想用這個參數,同樣也能夠設置到請求裏,設置了 Expires 的請求頭大概是這樣的nginx

若是請求的時候找到緩存文件了,而且查看緩存的時間未過時,則不會再次給服務發起請求,而是直接使用緩存。

cache-Control

上面的 response header 圖中可見,也設置了最大的緩存時間的 cache-control:max-age=3600

  1. max-age=num 設置最大緩存時間
  2. public 緩存能被多用戶共享
  3. private 緩存不能在多用戶間共享
  4. no-cache 緩存前必需確認緩存的有效性
  5. no-store 不能被存儲

last-modified

設置對象的最後修改時間

若是咱們啓用了協商緩存,它會在首次請求的時候被攜帶在響應頭裏,以後咱們每次請求都會帶上一個 if-modified-since 時間戳,服務器接收到這個值後會把先後兩次時間戳進行對比,判斷文件資源是否變化

last-modified 也會有弊端,若是咱們修改文件的時間過快或者修改了文件,但內容沒有變化時,last-modified 的時間就不能處理文件是否發生變化了,這個時候 Etag 就誕生了

Etag

文件 hash 值

經過文件 hash 值判斷緩存文件是否修改,從而判斷是否請求新資源

若是還存在一些還在使用 http 1.0 的場景的話,Etag 將不會起做用。

資源緩存方案

  1. HTML(no-cache + etag/last-modified)
  2. css、js(md5碼/timestamp/version + 長緩存)
  3. Image(隨機名字+長緩存)

請求包優化

衆所周知,請求資源時,若是資源越大,延時越大

代碼壓縮/合併(css、js)

如今的工程化已經可以作到幫咱們處理這一項了。

gzip

減小文件大小。gzip壓縮比率在3到10倍左右,能夠大大節省服務器的網絡帶寬。

須要服務器和瀏覽器支持,對文件進行壓縮後返回到用戶端解壓加載

通常在小的文件上配置 gzip 是沒有必要的,畢竟若是服務器壓縮,瀏覽器解壓再加上加載文件的總時間都超過直接加載文件的時間的話,使用 gzip 還有什麼意義呢?

若需配置 gzip,在請求的 headers 中加上這麼一句就能夠了

accept-encoding:gzip
複製代碼

若是使用 nginx 做爲 web 服務器的話,可在 nginx.conf 中對文件進行配置

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    gzip  on;   #開啓gzip
    gzip_min_length 1k; #低於1kb的資源不壓縮
    gzip_comp_level 3; #壓縮級別【1-9】,越大壓縮率越高,同時消耗cpu資源也越多,建議設置在4左右。
    #須要壓縮哪些響應類型的資源,多個空格隔開。不建議壓縮圖片,由於圖片壓縮後不只下降不了多少文件大小,反而還佔用了大量的服務器壓縮資源。
    gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
    
    gzip_disable "MSIE [1-6]\.";  #配置禁用gzip條件,支持正則。此處表示ie6及如下不啓用gzip(由於ie低版本不支持)
    gzip_vary on;  #是否添加「Vary: Accept-Encoding」響應頭

    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
複製代碼

gzip也有一個很是明顯的缺點,畢竟不論是服務器的壓縮仍是瀏覽器的解壓,都會佔用服務端和客戶端的 CPU 資源。因此須要根據自身狀況考慮後再決定是否有必要使用 gzip。

圖片優化

圖片通常是頁面中最大的資源,因此圖片的優化很重要

在加載圖片的時候,可經過配置(通常由建立圖片的角色操做)漸進式圖片提升用戶的體驗。關於漸進式圖片的配置和使用效果可參見張鑫旭的漸進式圖片及其相關

不少人會以爲 webp 的圖片的大小會小不少,因此加載會很快,但webp的兼容性並很差

圖標型圖片

若是使用圖標型圖片時,能夠經過2種方式:

  1. 雪碧圖
  2. iconfont 圖標字體(推薦)

小圖 -> 大圖

在通常的場景下,咱們均可以使用視覺欺詐的方式去處理圖片

  • ⼩圖是⼤圖的縮略圖,而後放到⼤圖的⼤⼩
  • ⼤圖保存成圖⽚漸進模式
  • ⼩圖 onload 以後再加載⼤圖,加載完成後直接替換⼩圖

其餘

從網絡層面,咱們前端能作獲得的優化很是有限。相比之下,HTTP 鏈接這一層面的優化纔是咱們網絡優化的核心。

  • 減小請求次數

前面說的資源合併,合理利用瀏覽器的並行請求數量

  • 減小單次請求所花費的時間

前面說的資源壓縮

webpack 工程優化

如今大部分工程都會使用 webpack 打包處理。你們在使用時會不會以爲打包的過程太長?打包完的文件體積太大?

優化構建速度

resolve

資源搜索過程優化

  • resolve.modules

告訴webpack去哪些目錄下尋找第三方模塊

默認值爲['node_modules'],會依次查找 ./node_modules、 ../node_modules、 ../../node_modules

resolve.modules:[path.resolve(__dirname, 'node_modules')]
複製代碼
  • resolve.alias

能夠給一些包(開發包/依賴包)設置別名,使得 webpack 在打包時查找文件時無需層層查找

resolve.alias:{
    '@pages':patch.resolve(__dirname, '/src/pages')
}
複製代碼

module

  • module.noParse

告訴Webpack沒必要解析哪些文件,能夠用來排除對非模塊化庫文件的解析

module:{ noParse:[/webim\.min\.js$/,/chart\.js$/] }
複製代碼

loader

  • include、exclude

減小沒必要要的轉譯

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}
複製代碼
  • babel-loader 參數:cacheDirectory

緩存沒有發生改變的文件的轉譯資源,無需再次轉譯

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader?cacheDirectory=true',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}
複製代碼
  • HappyPack

webpack 是單線程的,就算存在多個任務,也是排隊依次進行處理。而 webpack 打包過程當中,loader 解析最耗時。

HappyPack 能夠充分利用 CPU 在多核併發的優點,把任務分解給多個子進程去併發的執行,子進程處理完後再把結果發送給主進程

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // 把對 .js 文件的處理轉交給 id 爲 babel 的 HappyPack 實例
        use: ['happypack/loader?id=babel'],
        // 排除 node_modules 目錄下的文件,node_modules 目錄下的文件都是採用的 ES5 語法,不必再經過 Babel 去轉換
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        // 把對 .css 文件的處理轉交給 id 爲 css 的 HappyPack 實例
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css'],
        }),
      },
    ]
  },
  plugins: [
    new HappyPack({
      // 用惟一的標識符 id 來表明當前的 HappyPack 是用來處理一類特定的文件
      id: 'babel',
      // 如何處理 .js 文件,用法和 Loader 配置中同樣
      loaders: ['babel-loader?cacheDirectory'],
      // ... 其它配置項
    }),
    new HappyPack({
      id: 'css',
      // 如何處理 .css 文件,用法和 Loader 配置中同樣
      loaders: ['css-loader'],
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,
    }),
  ],
};
複製代碼

DllPlugin

DllPlugin 插件會把第三方庫單獨打包到一個文件中,這個文件就是一個單純的依賴庫。這個依賴庫不會跟着業務代碼一塊兒被從新打包,只有當依賴自身發生版本變化時纔會從新打包。

注: 該插件主要使用的是已打包好的文件緩存

用 DllPlugin 處理文件,要分兩步走:

  • 基於 dll 專屬的配置文件,打包 dll 庫
// webpack_dll.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
  mode: 'production',
  entry: {
    lodash: ['lodash'],
    jquery: ['jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, '../dll/[name].manifest.json') // 用這個插件來分析打包後的這個庫,把庫裏的第三方映射關係放在了這個 json 的文件下,這個文件在 dll 目錄下
    })
  ]
}

複製代碼

配置執行 webpack_dll.config.js 文件的指令,並執行

"scripts": {
    "build:dll": "webpack --config ./build/webpack.dll.js"
}
複製代碼

最終構建出的文件:

|-- jquery.dll.js
 |-- jquery.manifest.json
 |-- lodash.dll.js
 └── lodash.manifest.json
複製代碼
  • 基於 webpack.config.js 文件,打包業務代碼

在主config文件裏使用 DllReferencePlugin 插件引入 xx.manifest.json 文件

//webpack.config.js
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
    entry:{ main:'./main.js' },
    //... 省略output、loader等的配置
    plugins:[
        new DllReferencePlugin({
            // manifest就是咱們第一步中打包出來的json文件
            manifest:require('./dist/jquery.manifest.json')
        }),
        new DllReferenctPlugin({
            // manifest就是咱們第一步中打包出來的json文件
            manifest:require('./dist/lodash.manifest.json')
        })
    ]
}
複製代碼

打包後的文件體積優化

包組成可視化工具——webpack-bundle-analyzer,它會以矩形樹圖的形式將包內各個模塊的大小和依賴關係呈現出來,截取官網圖以下:

在使用時,咱們只須要將其以插件的形式引入:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
        openAnalyzer: true,
    })
  ]
}
複製代碼

壓縮

  • UglifyJsPlugin

在 webpack3 中,引入 UglifyJsPlugin 插件對 js 進行壓縮,webpack4 如今已經默認使用 uglifyjs-webpack-plugin 對代碼作壓縮了—— 在 webpack4 中,咱們是經過配置 optimization.minimize 與 optimization.minimizer 來自定義壓縮相關的操做。

module.exports = {
  //... 省略其餘配置
  optimization: {
    runtimeChunk: {
      name: 'manifest'
    },
    minimizer: true, // [new UglifyJsPlugin({...})]
    splitChunks:{
      chunks: 'async',
    }
  }
}
複製代碼

移除 JavaScript 上下文中的未引用代碼(dead-code),從而減小打包後文件的體積。它依賴於 ES2015 模塊系統中的靜態結構特性,例如 import 和 export。

從定義中能夠看出,Tree-Shaking 的針對性很強,它更適合用來處理模塊級別的冗餘代碼。至於粒度更細的冗餘代碼的去除,能夠經過配置插件對 js 和 css 進行壓縮分離處理,如上面的 UglifyJsPlugin。

  • compresion-webpack-plugin

使用不一樣的算法對壓縮後的文件進行再壓縮

const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin')

module.exports = {
    plugins: [
        new CompressionPlugin({
            test: /\.(js|css|html)$/,
            // include: /\/src/,
            filename: '[path].gz[query]',
            algorithm: 'gzip', //算法
            // threshold: 8192,
        }),
    ]
}
複製代碼

使用該插件後的文件大小差異以下圖所示,能夠看出至少減小了 一半 的大小,使用壓縮文件也有一些缺點,在上面的gzip中也有提到。

按需加載

主要場景:單頁應用

舉個例子:使用 vue 構建一個單頁應用,其中有十個路由,經過 vue-router 控制這些路由,每一個路由對應的頁面的業務都不簡單。打包發佈這個項目後,打開網站極大機率會出現長時間等待

這時候咱們能夠選擇按需加載路由或組件,當前路由對應的頁面只加載當前頁面相關的組件或路由。

// 按需加載路由
{
    path: '/promisedemo',
    name: 'PromiseDemo',
    component: () => import('../components/PromiseDemo')
    // 或者使用下面的寫法
    // component: resolve => resolve(require('../components/PromiseDemo'))
},
{            
    path: '/hello',
    name: 'Hello',
    // component: Hello
    component: () => import('../components/Hello')
    // 或者使用下面的寫法
    // component: resolve => resolve(require('../components/Hello'))
}

複製代碼

有時候咱們想把某個路由下的全部組件都打包在同個異步塊 (chunk) 中。只須要使用 命名 chunk,一個特殊的註釋語法來提供 chunk name (須要 Webpack > 2.4)。引自:組件按組分塊

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
複製代碼
相關文章
相關標籤/搜索