前端优化

351 阅读13分钟

前端性能优化可以分为两大类分别是页面级别优化包含了http请求数以及内联脚本位置优化,代码级别的优化包含DOM操作优化,CSS选择符优化以及图片优化等

前端里面包含的内容是丰富的,它包括HTML,CSS以及JS和图片等各种各样的资源。因此前端优化是复杂的和必要的

1. 减少 HTTP 请求

            

  • Queueing: 在请求队列中的时间。
  • Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
  • Proxy negotiation: 与代理服务器连接进行协商所花费的时间。
  • DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
  • Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
  • SSL: 完成SSL握手所花费的时间。
  • Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。
  • Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间总和,它包含了 DNS 解析时间、 TCP 连接时间、发送 HTTP 请求时间和获得响应消息第一个字节的时间。
  • Content Download: 接收响应数据所花费的时间。

从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。

http协议是无状态的应用层协议,意味着每次http请求都需要建立通信链路、进行数据传输,而在服务器端,每个http都需要启动独立的线程去处理。这些通信和服务的开销都很昂贵,减少http请求的数目可有效提高访问性能。 减少http的主要手段是合并CSS、合并javascript、合并图片。将浏览器一次访问需要的javascript和CSS合并成一个文件,这样浏览器就只需要一次请求。图片也可以合并,多张图片合并成一张,如果每张图片都有不同的超链接,可通过CSS偏移响应鼠标点击操作,构造不同的URL。 缓存的力量是强大的,恰当的缓存设置可以大大的减少 HTTP请求。假设某网站首页,当浏览器没有缓存的时候访问一共会发出 78个请求,共 600多 K数据,而当第二次访问即浏览器已缓存之后访问则仅有 10个请求,共 20多 K数据。 (这里需要说明的是,如果直接 F5刷新页面的话效果是不一样的,这种情况下请求数还是一样,不过被缓存资源的请求服务器是 304响应,只有 Header没有Body ,可以节省带宽 ) 

2.静态资源使用 CDN

内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。

3.将 CSS 放在文件头部,JavaScript 文件放在底部

所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。

那为什么 CSS 文件还要放在头部呢?

因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。

另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。

4.压缩文件

压缩文件可以减少文件下载时间,让用户体验性更好。

得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。

5.图片优化

  • 雪碧图;
  • 压缩图片(tinypng.com/);
  • 不影响页面质感的前提下降低图片质量;
  • 小图标使用阿里矢量图的iconfont
  • 预加载与懒加载

6.减少重绘重排

重排

当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。

重绘

当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。记住,重排会导致重绘,重绘不会导致重排 。

重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。

什么操作会导致重排?

  • 添加或删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变
  • 内容改变
  • 浏览器窗口尺寸改变

如何减少重排重绘?

  • 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  • 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。

7.优化首屏加载时间

  1. cdn分发(减少传输距离)。通过在多台服务器部署相同的副本,当用户访问时,服务器根据用户跟哪台服务器距离近,来决定哪台服务器去响应这个请求。

  2. 后端在业务层的缓存。数据库查询缓存是可以设置缓存的,这个对于处于高频率的请求很有用。浏览器一般不会对content-type: application/json的接口进行缓存,所以有时需要我们手动地为接口设置缓存。比如一个用户的签到状态,它的缓存时间可以设置到明天之前。

  3. 静态文件缓存方案。这个最常看到。现在流行的方式是文件hash+强缓存的一个方案。比如hash+ cache control: max-age=1年。

  4. 前端的资源动态加载:

    a. 路由动态加载,最常用的做法,以页面为单位,进行动态加载。
    b. 组件动态加载(offScreen Component),对于不在当前视窗的组件,先不加载。
    c. 图片懒加载(offScreen Image),同上。值得庆幸的是,越来越多的浏览器支持原生的懒
       加载,通过给img标签加上loading="lazy来开启懒加载模式。 
    
  5. 合并请求。这点在http1.1比较明显,因为http1.1的请求是[串行

    ](www.zhihu.com/search?q=%E…

  6.  页面使用骨架屏。意思是在首屏加载完成之前,通过渲染一些简单元素进行占位。骨架屏的好处在于可以减少用户等待时的急躁情绪。这点很有效,在很多成熟的网站都有大量应用。没有骨架屏的话,一个loading图也是可以的。

  7. 使用ssr服务端渲染。(www.jianshu.com/p/10b6074d7…

  8. 引入http2.0。http2.0对比http1.1,最主要的提升是传输性能,在接口小而多的时候会更加明显。

  9. 利用好http压缩。即使是最普通的gzip,也能把bootstrap.min.css压缩到原来的17%。可见,压缩的效果非常明显,特别是对于文本类的静态资源。另外,接口也是能压缩的。接口不大的话不用压缩,因为性价比低(考虑压缩和解压的时间)。

  10. 利用好script标签的async和defer这两个属性。功能独立且不要求马上执行的js文件,可以加入async属性。如果是优先级低且没有依赖的js,可以加入defer属性。

  11. (少用)选择先进的图片格式。使用 WebP 的图片格式来代替现有的jpeg和png,当页面图片较多时,这点作用非常明显。把部分大容量的图片从BaseLine JPEG切换成Progressive JPEG也能缩小体积。

8.webpack打包优化

1.禁用webpack的devtools

  • 打包出来的js文件非常大,每个js文件竟然达到了3~4Mbs

  • 究其原因,是因为webpack里面启用了sourceMap,以便于调试。但是这在发布以后就完全没有用了。

  • webpack配置,里面有这句话,把这句话注释掉。原本3~4Mbs的文件,变成了1Mbs文件。压缩了3倍以上。

    // 启用sourceMap devtool: '#source-map'

2. 抽离css样式等

// *************webpack需要引入的包*************************
// 抽离css样式
let MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 用来压缩分离出来的css样式
let OptimizeCss =  require('optimize-css-assets-webpack-plugin');
// 用来压缩js
let UglifyJsPlugin = require('uglifyjs-webpack-plugin');

// *************webpack相关配置部分*************************
module.exports = {
    optimization: {
        // 优化项
        minimizer: [
            new OptimizeCss(), // 压缩css
            new UglifyJsPlugin({ // 压缩js
                cache: true, // 是否用缓存
                parallel: true, // 并发打包
                sourceMap: false, // es6 -> es5 转换时会用到
            }),
        ],
    }
    // 中间部分省略

    // 抽离css样式
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].css', // 抽离出来样式的名字
        }),
    ],
}

3.启用依赖关系可视化工具(webpack-bundle-analyzer

// 依赖关系可视化
// *************webpack需要引入的包*************************
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

// *************webpack相关配置部分*************************
module.exports = {

    // 启动依赖关系可视化窗口,绑定端口8919
    plugins: [
        new BundleAnalyzerPlugin({ analyzerPort: 8919 }),
    ],
}

根据出现的可视化图可以看到哪些文件比较大,从而进行细化优化

分析包大小问题:

  • ant-design的Icons文件
  • moment文件
  • quill文件
  • highlight.js文件

3.1 ant-design的Icons文件优化

  • 按需引入需要的icon文件

    // 自己项目里面用到的Icon export { default as FileOutline, } from '@ant-design/icons/lib/outline/FileOutline';

    // antd的message组建内部用到的Icon 把源代码复制过来。 // var iconType = { // info: 'info-circle', // success: 'check-circle', // error: 'close-circle', // warning: 'exclamation-circle', // loading: 'loading' // }[args.type]

    // message info export { default as InfoCircleTwoTone, } from '@ant-design/icons/lib/twotone/InfoCircleTwoTone';

    // message success export { default as CheckCircleTwoTone, } from '@ant-design/icons/lib/twotone/CheckCircleTwoTone';

    // message error export { default as CloseCircleTwoTone, } from '@ant-design/icons/lib/twotone/CloseCircleTwoTone';

    // message warning export { default as ExclamationCircleTwoTone, } from '@ant-design/icons/lib/twotone/ExclamationCircleTwoTone';

    // message loading export { default as LoadingOutline, } from '@ant-design/icons/lib/outline/LoadingOutline';

  • 修改 webpack 配置

    module.exports = { resolve: { modules: [path.resolve(__dirname, './src'), 'node_modules'], // <- 追加代码 extensions: ['.ts', '.js', '.vue', '.json'], // <- 追加代码 alias: { vue:vue/dist/vue.esm.js,@antdesign/icons/lib/dist: 'vue/dist/vue.esm.js', '@ant-design/icons/lib/dist': path.resolve(__dirname, './src/tools/antdIcon.ts'), // <- 追加代码 }, plugins: [ // <- 追加代码 new TsconfigPathsPlugin({ configFile: path.resolve(__dirname, './tsconfig.json'), }), ], },

3.2 moment文件优化

  • 这是个Ant-design内部依赖的语言文件,我的程序里面本身没有引用,我主要用到的是里面的中文,所以,中文以外的我全部在webpack里面设置忽略就行了

    module.exports = { plugins: [ // 只读取(zh-cn)文件。 new webpack.ContextReplacementPlugin(/moment[\/]locale/, /^\.\/(zh-cn)/), ], }

3.3 quill文件优化

  • 这个是我用到的富文本功能,本身对这个组件不太了解,但又必须要用到,也没什么太好优化方法,索性,把它抽离成一个单独的共通JS文件,这样起码有2个组件同时调用这个富文本的情况下,只有第一个掉用的那个需要引入JS文件,第二次的那个会直接利用浏览器的缓存来调用这个JS文件的,也有一定程度的优化效果。

    module.exports = { optimization: { splitChunks: { cacheGroups: { vendor: { chunks: 'all', test: /\/[\/]/, // <- 就是简单修改了下匹配规则 name: 'vendor', minChunks: 1, maxInitialRequests: 5, minSize: 0, priority: 100, }, common: { chunks: 'all', test: /[\/]src[\/]js[\/]/, name: 'common', minChunks: 2, maxInitialRequests: 5, minSize: 0, priority: 1, }, }, }, runtimeChunk: { name: 'manifest', }, } }

3.4 highlight.js文件优化

  • 这个主要是我用markdown编辑器的时候,用来给文字着色的。没有这个,在编写markdown的时候,内容非常的丑陋。

  • 但是这个东西的语法太多了,导致这个包非常的大,我编写的时候,只需要利用其中的几种情况而已,我先随便定几种情况,反正是自己的项目,有不够的以后随时再追加

    // 按需加载的写法 import hljs from 'highlight.js/lib/highlight'; import javascript from 'highlight.js/lib/languages/javascript'; hljs.registerLanguage('javascript', javascript);

4.抽离共通部分

为了方便我调查,我把共通的依赖部分都抽离出来了。而这个功能是webpack4自带的,如果是之前或者更早版本的webpack,需要引入第三方组件CommonsChunkPlugin这里不做介绍。

module.exports = {
    optimization: {
        //打包 公共文件
        splitChunks: {
            cacheGroups: {
                vendor: {
                    //node_modules内的依赖库
                    chunks: 'all',
                    test: /[\\/](node_modules)[\\/]/, // 文件路径里面带有node_modules 都抽离出来做共通
                    name: 'vendor',
                    minChunks: 1, //被不同entry引用次数(import),1次的话没必要提取
                    maxInitialRequests: 5,
                    minSize: 0,
                    priority: 100,
                    // enforce: true?
                },
                common: {
                    // ‘src/js’ 下的js文件
                    chunks: 'all',
                    test: /[\\/]src[\\/]js[\\/]/, //也可以值文件/[\\/]src[\\/]js[\\/].*\.js/,
                    name: 'common', //生成文件名,依据output规则
                    minChunks: 2,
                    maxInitialRequests: 5,
                    minSize: 0,
                    priority: 1,
                },
            },
        },
        runtimeChunk: {
            name: 'manifest',
        },
    }
}

5.配置 resolve.modules(大约缩短十几秒)

(1)webpack 的 resolve.modules 是用来配置模块库(即 node_modules)所在的位置。当 js 里出现 import 'vue' 这样不是相对、也不是绝对路径的写法时,它便会到 node_modules 目录下去找。

(2)在默认配置下,webpack 会采用向上递归搜索的方式去寻找。但通常项目目录里只有一个 node_modules,且是在项目根目录。为了减少搜索范围,可我们以直接写明 node_modules 的全路径。

打开 build/webpack.base.conf.js 文件:

module.exports = {  resolve: {    extensions: ['.js', '.vue', '.json'],    modules: [      resolve('src'),      resolve('node_modules')    ],    alias: {      'vue$': 'vue/dist/vue.esm.js',      '@': resolve('src'),    }  },

6.配置装载机的 include & exclude

(1)webpack 的装载机(loaders)里的每个子项都可以有 include 和 exclude 属性:

  • include:导入的文件将由加载程序转换的路径或文件数组(把要处理的目录包括进来)
  • exclude:不能满足的条件(排除不处理的目录)

(2)我们可以使用 include 更精确地指定要处理的目录,这可以减少不必要的遍历,从而减少性能损失。

(3)同时使用 exclude 对于已经明确知道的,不需要处理的目录,予以排除,从而进一步提升性能。

打开 build/webpack.base.conf.js 文件,第 7 行的 easytable 如果项目没用到可以把它从 include 中去除。

module: {  rules: [    {      test: /\.vue$/,      loader: 'vue-loader',      options: vueLoaderConfig,      include: [resolve('src'), resolve('node_modules/vue-easytable/libs')],      exclude: /node_modules\/(?!(autotrack|dom-utils))|vendor\.dll\.js/    },    {      test: /\.js$/,      loader: 'babel-loader',      include: [resolve('src')],      exclude: /node_modules/    },

7.使用 webpack-parallel-uglify-plugin 插件来压缩代码(可以缩短50多秒)

1)默认情况下 webpack 使用 UglifyJS 插件进行代码压缩,但由于其采用单线程压缩,速度很慢。

(2)我们可以改用 webpack-parallel-uglify-plugin 插件,它可以并行运行 UglifyJS 插件,从而更加充分、合理的使用 CPU 资源,从而大大减少构建时间。

打开 build/webpack.prod.conf.js 文件,并作如下修改:

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');//....    // 删掉webpack提供的UglifyJS插件    //new UglifyJsPlugin({    //  uglifyOptions: {    //    compress: {    //      warnings: false    //    }    //  },    //  sourceMap: config.build.productionSourceMap,    //  parallel: true    //}),    // 增加 webpack-parallel-uglify-plugin来替换    new ParallelUglifyPlugin({      cacheDir: '.cache/',      uglifyJS:{        output: {          comments: false        },        compress: {          warnings: false        }      }    }),

8.使用 HappyPack 来加速代码构建

(1)由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的事情只能一件一件地做,不能多件事一起做。

(2)而 HappyPack 的处理思路是:将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展多进程模式,从而加速代码构建。

打开 build/webpack.base.conf.js 文件,并作如下修改:

const HappyPack = require('happypack');const os = require('os');const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); module.exports = {  module: {    rules: [      {        test: /\.js$/,        //把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行        loader: 'happypack/loader?id=happyBabel',        include: [resolve('src')],        //排除node_modules 目录下的文件        exclude: /node_modules/      },    ]  },  plugins: [    new HappyPack({        //用id来标识 happypack处理那里类文件      id: 'happyBabel',      //如何处理  用法和loader 的配置一样      loaders: [{        loader: 'babel-loader?cacheDirectory=true',      }],      //共享进程池      threadPool: happyThreadPool,      //允许 HappyPack 输出日志      verbose: true,    })  ]}

9.利用 DllPlugin 和 DllReferencePlugin 预编译资源模块

(1)我们的项目依赖中通常会引用大量的 npm 包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,而下面介绍的两个插件就是用来规避此类损耗的:

  • DllPlugin 插件:作用是预先编译一些模块。
  • DllReferencePlugin 插件:它的所用则是把这些预先编译好的模块引用起来。

(2)注意:DllPlugin 必须要在 DllReferencePlugin 执行前先执行一次,dll 这个概念应该也是借鉴了 windows 程序开发中的 dll 文件的设计理念。

在 build 文件夹中新建 webpack.dll.conf.js 文件,内容如下(主要是配置下需要提前编译打包的库):

const path = require('path');const webpack = require('webpack'); module.exports = {  entry: {    vendor: ['vue/dist/vue.common.js',            'vue-router',            'axios',            'mint-ui',            'vue-cordova',            '@fortawesome/fontawesome-svg-core',            '@fortawesome/free-solid-svg-icons',            '@fortawesome/free-regular-svg-icons',            '@fortawesome/free-brands-svg-icons',            '@fortawesome/vue-fontawesome']  },  output: {    path: path.join(__dirname, '../static/js'),    filename: '[name].dll.js',    library: '[name]_library'       // vendor.dll.js中暴露出的全局变量名  },  plugins: [    new webpack.DllPlugin({      path: path.join(__dirname, '.', '[name]-manifest.json'),      name: '[name]_library'    }),    new webpack.optimize.UglifyJsPlugin({      compress: {        warnings: false      }    })  ]};

编辑 package.json 文件,添加一条编译命令:

{  "name": "ddjk_vue",  "version": "1.0.0",  "description": "A Vue.js project",  "author": "Boss",  "private": true,  "scripts": {    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",    "start": "npm run dev",    "build": "node build/build.js",    "build:dll": "webpack --config build/webpack.dll.conf.js"  },

执行 npm run build:dll 命令来生成 vendor.dll.js。

注意:如果之后这些需要预编译的库又有变动,则需再次执行 npm run build:dll 命令来重新生成 vendor.dll.js

index.html 这边将 vendor.dll.js 引入进来。

打开 build/webpack.base.conf.js 文件,编辑添加如下高亮配置,作用是通过 DLLReferencePlugin 来使用 DllPlugin 生成的 DLL Bundle。

const webpack = require('webpack'); module.exports = {  context: path.resolve(__dirname, '../'),  entry: {    app: './src/main.js'  },  //.....  plugins: [     // 添加DllReferencePlugin插件     new webpack.DllReferencePlugin({       context: path.resolve(__dirname, '..'),       manifest: require('./vendor-manifest.json')     }),  ]}