杂谈:前端性能优化的思路

lxf2023-02-17 01:51:17

高频面试题:在以往开发的项目中做过哪些优化?

为什么要做优化,大抵会有以下一些原因:

  • 白屏时间过长
  • 页面卡顿

这些问题都会让客户浏览体验很差,轻则关闭,重则将永远失去这个用户,这就是为什么要做网站性能优化的原因

那么,我们该从哪些方面入手呢?

其实,从浏览器地址栏输入url到页面展示的过程中,主要就是拉取资源体积的大小,请求速度和资源组织方式这三个方面的问题,可以从这入手。

一、http请求

1、采用域名分片技术

如果我们不得不使用http1协议时,可以通过域名分片技术,将资源放到多个域名下,因为一个域名最多可以开启6个TCP连接,这样资源请求的过程中多个域名就会开启多个TCP连接,一定程度上可以让请求速度变快。

2、http1升级成http2

因为http2通过引入二进制分帧层,就实现了 HTTP 的多路复用技术,该技术可以设置请求的优先级、进行主要资源的服务器推送和头部压缩,所以http1升级成http2可以让请求速度更快。

3、开启keep-alive

一般服务端向客户端返回了数据以后,就会关闭TCP,客户端再次发出请求时,会再次启动TCP,我们知道TCP的启动是慢启动,所以我们通过keep-alive让TCP保持打开状态,提升资源加载速度。

4、开启gzip压缩

可以通过开启gzip压缩,让客户端拉取到的数据是经过压缩的,减小文件体积,进而提升资源获取速度。

5、减少无用的请求头数据

客户端在向服务端获取数据时,会携带请求数据,有些无用的token、cookie信息或者请求体中的数据都可以去除。

二、基础优化

6、非重要文件采用异步加载方式

在.html文件中,有些资源是渲染页面必须的,有些资源可以迟一些再加载,为了减小阻塞,可以通过defer或者async的方式让非重要文件采用异步加载的方式,优化资源组织方式。

7、删除辅助开发的console信息

在平时开发的时候,会通过console.log等方式去打印信息,在上生产环境时,需要删除这些信息,以减小文件体积。

8、script文件放置位置

因为javascript是单线程语言,计算量比较大或者加载比较耗时的script资源会阻塞文件的DOM渲染,为了让页面先进行展示,我们可以将script文件放置到body结束标签之前。

9、css样式采用媒体查询

需要根据不同场景进行样式展示时,尽量避免通过javascript的方式去判断,并加载css资源,而应该采取媒体查询的方式,以减小文件体积。

10、服务端渲染优化

如果是服务端的SSR项目,可以从减小资源体积方面考虑服务端渲染有没有可优化的点。

11、注意代码规范

抽取公共组件,公共js,公共css样式,减小代码体积。删除无用代码,减少非必要注释。防止写出死循环等等

12、字体图标的使用

有些图片图标尽可能使用字体图标,图片的体积一般都会比字体图标大

三、框架特性(vue)

13、v-if和v-show

频繁切换时使用v-show,利用其缓存特性,减小切换开销;需要考虑首屏渲染的场景时使用v-if,如果为false则不进行渲染,可以减小节点数量。

14、v-for的key

列表变化时,循环时使用唯一不变的key,借助其本地复用策略,减小创建新节点的开销,当然列表只进行一次渲染时,key采用循环的index也可以。

15、侦听器和计算属性

侦听器watch用于数据变化时引起其他行为;compouter计算属性顾名思义就是新计算而来的属性,如果依赖的数据未发生变化,不会触发重新计算,所以在依赖数据计算获取新数据的场景可以选择compouter

16、合理使用生命周期

destroyed阶段进行绑定事件或者定时器的销毁;使用动态组件的时候通过keep-alive包裹进行缓存处理,相关的操作可以在actived阶段激活。

17、数据响应式处理

不需要响应式处理的数据可以通过Object.freeze处理,或者直接通过this.xxx = xxx的方式进行定义;需要响应式处理的属性可以通过this.$set的方式处理,而不是JSON.parse(JSON.stringify(XXX))的方式。

18、路由加载方式

页面路由组件可以采用懒加载的方式,只有请求到当前路由资源时,才去服务器拉取数据。

19、异步组件的使用

页面渲染过程中可以先让重要的页面结构进行渲染,非重要页面采取异步组件的方式。

20、插件引入方式

第三方插件可以采用按需加载的方式,比如element-ui

21、减少代码量

  • 采用mixin的方式抽离公共方法
  • 抽离公共组件
  • 定义公共方法至公共js
  • 抽离公共css

22、编译方式

如果线上需要template的编译,可以采用完成版vue.esm.js;如果线上无需template的编译,可采用运行时版本vue.runtime.esm.js,相比完整版体积要小大约30%

23、渲染方式

一些企业内部使用的后端管理系统可以采用前端渲染的方式;如果是需要考虑首屏加载或者SEO的网站可以采用服务端渲染的方式。

四、打包工具(webpack)

24、环境切换

在开发环境中我们打开source map的目的是为了追踪错误的具体文件和文件中的具体位置,devtool配置是:

module.exports = {
    mode: 'production',
    devtool: 'source-map',
    // 省略其他配置
};

那么打包的文件中就会有.map文件

杂谈:前端性能优化的思路

如果在上线打包前忘记关掉source map,那么,打包后的文件中就会产生.map文件。

如果上线前我们注释了devtool: 'source-map',那么,打包后的文件中就没有了.map文件。

结论:关闭source map不会产生.map文件,进而减小打包后的文件体积。

25、管理输出

假设我们的webpack的配置有两个入口print.jsindex.js,同时,也开启了source map

module.exports = {
    mode: 'development',
    devtool: 'source-map', // 产生.map文件
    entry: {
        index: './src/index.js',
        print: './src/print.js', // 产生print.js的打包文件
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
};

此时打包后的文件会有:

杂谈:前端性能优化的思路

此时,我们关闭source map,并且,删掉了print.js文件,配置如下:

module.exports = {
    mode: 'development', 
    // devtool: 'source-map', // 注释了,就没有.map文件了
    entry: {
        index: './src/index.js',
        // print: './src/print.js', // 注释了,就应该没有print.js的打包文件啦
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
};

但是,打包后的文件夹下,之前的文件依然存在。

假设,上线前忘记打开文件自动清除,那么,线上包的体积会比较大。

相信不会忘的,当我们在输出项output中配置clean:true时,打包后的结果为:

杂谈:前端性能优化的思路

结论:打包输出项中配置clean:true可以只产生需要的文件,清除遗留的无用的文件,达到减小打包文件的体积。

26、代码分割

我们知道客户端拉取服务端资源的时候,如果包的体积很大,会影响加载速度,webpack可以通过代码分割的方式来生成多个小文件,可以通过控制各个资源加载的优先级,提高资源加载速度。代码分割实现方式主要有以下几种:

假如我们有另外一个文件anthoer-module.js。

// anthoer-module.js文件
import _ from 'lodash';
console.log(_.join(['Another', 'module', 'loaded!'], ' '));

(1)入口起点

我们配置入口文件:

entry: {
    index: './src/index.js',
    another: './src/another-module.js',
},

此时执行的结果为:

杂谈:前端性能优化的思路

(2)防止重复

虽然以上方式能够完成代码分割的目的,但是,会存在以下隐患:

  • 如果每个文件中都有重复的文件,那些重复模块都会被引入到各个bundle中。
  • 以上方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。

这种问题可以通过以下两种方式解决:

①配置dependOn
entry: {
    index: {
        import: './src/index.js',
        dependOn: 'shared',
    },
    another: {
        import: './src/another-module.js',
        dependOn: 'shared',
    },
    shared: 'lodash',
},

此时,打包的文件中就会多一个公共的文件,作为公共模块lodash.js文件:

杂谈:前端性能优化的思路

SplitChunksPlugin

配置optimization.splitChunks选项:

entry: {
    index: './src/index.js',
    another: './src/another-module.js',
},
optimization: {
    splitChunks: {
        chunks: 'all',
    },
},

执行结果如下:

杂谈:前端性能优化的思路

此时,打包的文件中就会多一个公共的文件,作为公共模块venders类的文件:

(3)动态导入

  • import语法:通过动态导入的方式,也可以实现代码的分割。

27、缓存

当打包的文件放入到服务器后,如果客户端每次访问都从服务端拉取资源的话,也许每次都拉取的是没有更新过的资源。如果,每次拉取的都是更新过的资源,而未更新的资源使用缓存,那么就会提高二次访问时的效率,用户直观感受可能是第二次白屏时间更短。下面介绍其可实现的思路:

(1)输出文件的文件名

通过配置output.filename的方式为其增加.[contenthash],只有当资源内容发生改变时才会让打包后的文件名发生改变:

output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
},

首次加载资源时:

杂谈:前端性能优化的思路

再次加载资源时:

杂谈:前端性能优化的思路

通过对比可以发现,第二次加载时资源加载状态为304,利用了缓存,减小了资源数据的拉取。

(2)提取引导模板

通过runtimeChunk: 'single'的方式为所有打包后的bundle创建一个runtime bundle,并且通过配置optimization.splitChunks.cacheGroups的方式将公共资源抽取到vendor chunk文件中。

optimization: {
    runtimeChunk: 'single',
    splitChunks: {
        cacheGroups: {
            vendor: {
                test: /[\\/]node_modules[\\/]/,
                name: 'vendors',
                chunks: 'all',
            },
        },
    },
},

结果为:

杂谈:前端性能优化的思路

因为例如lodash和vue这样的文件体积比较大且基本不发生变化的文件,拆成单独的vendor chunk文件,客户端可以使用其缓存。

28、tree-shaking

主要针对import或者export静态引入的语法,通过mode:'production'选项的配置可以自动开启tree-shaking,指的是未使用到的模块不会打包到bundle.js中去。

先创建一个公共文件math.js:

export function square(x) {
    return x * x;
}

export function cube(x) {
    return x * x * x;
}

然后在index.js文件中使用cube方法,但是不使用square方法:

import { cube } from './math.js';

function component() {
    const element = document.createElement('pre');
    element.innerHTML = cube(5);
    return element;
}

document.body.appendChild(component());

以上打包在mode:'development'模式下,会将square方法也打包进去。如果在mode:'production'模式下,不会将square方法打包进去。

结论:在线上运行时,通过开启mode:'production'的方式开启tree-shaking,可以减少资源包的体积。

29、预加载/预获取

Webpack v4.6.0+ 增加了对预获取和预加载的支持。 在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 "resource hint(资源提示)",来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

30、懒加载/按需加载

懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

一般是在某些触发行为发生后,再通过import的方式引入,引入的过程中再去服务端拉取资源。

31、shim/pollyfill

通过shim,可以仅仅将其中需要的方法作为全局变量,达到按需引入的目的。

32、压缩图片/压缩文件

在项目使用过程中,有时候一张图片有好几M,这无疑会降低页面渲染速度。可以在不降低用户体验的情况下,根据webpack中图片压缩的相关配置,进行压缩,以减小图片体积。

33、CSS的擦除

通过purgecss-webpack-plugin的方式对无用的css文件进行擦除。

总结

优化内容有些多,大体可以就拉取资源体积的大小,请求速度和资源组织方式角度去优化。而且,也可以通过浏览器开发者工具中的PerformanceLighthouse模块生成加载或者性能报告,根据其提示的思路去优化页面。

写在最后

性能优化的内容不仅这些,以上内容仅用来做个引子,希望给看到的学友提供些思路,纰漏之处在所难免,请批评指正。

如果本文有些帮助,请点赞+收藏。