为什么你的页面速度慢/操作卡/体积大?【前端性能优化思路】

3,793 阅读10分钟

开发时很经常会遇到一种情况:刚接手了一个现有的已发布至生产项目,就会被甩下三个问题

  • 为什么打开这么慢?
  • 为什么这么卡?
  • 为什么打出来的包这么大?

1.为什么打开这么慢?

这里的慢一般指的是,页面的白屏(FP)到首屏完整(FCP)加载加起来的时长。

问题分析

  1. 先问清楚问题的复现过程,偶现还是必现,偶现的话有没有什么具体的时间段或者网络是否正常。有无特定的复现机型、系统、浏览器。了解更详细的信息有助于我们排查问题

  2. 若没有解决的话就得从前端开始入手,导致慢的原因有很多种可能,用chrome打开页面,按f12,转到network,勾上Disable cache

image.png

network相关功能说明及使用技巧可以参照这篇文章。Chrome教程(一)NetWork面板分析网络请求

首先我们需要知道的是,浏览器加载的触发流程大致是根据以下步骤进行的:

  1. 解析 HTML 结构,并且构建成一棵DOM树。
  2. 加载外部脚本和样式表文件
  3. 解析并执行脚本代码 // 部分脚本会阻塞页面的加载
  4. DOM树构建完之后,浏览器把DOM树中的一些不可视元素去掉,然后与CSSOM合成一棵render tree。
  5. 加载图片等外部文件
  6. 页面进行渲染,加载完毕 // load事件

image.png 接着我们就可以看是什么内容的time占用时长最多,根据不同文件、接口的加载时间进行相对应的优化。

优化思路

1.网络问题
  • 压缩打包体积,减少请求的数量,降低服务器压力。【文章的第三大问会展开讲】
  • 开启gzip。在请求中的request headers 中加上accept-encoding:gzip,服务器收到此请求信息后,通过Gzip来对Response进行编码,可减少文件70%的体积
  • 服务器开启页面缓存,如果符合设置条件的话,之后前端的请求会直接获取浏览器的缓存。缓存分为强缓存和协商缓存。加快了客户端加载网页的速度的同时,也减少了服务器的负担,大大提升了网站的性能
//max-age=31536000 等同于页面在31536000时间戳之内。
cache-control: max-age=31536000
//expires 
expires: Wed, 11 Sep 2019 16:12:18 GMT
  • 把所有用到的图片、js文件等放在同一个域名的服务器,减少dns解析
  • 给CDN地址加上dns-prefetch,可以对DNS进行预解析。尽量把预解析的代码写到页面的最开始的部分,能尽快加载
//href只需要填你需要请求的域名//+你请求的域名,如下例
<link rel="dns-prefetch" href="//code.jquery.com" />

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>
  • 给需要首屏不需要使用到的js或者css文件,使用link+prefetch/preload进行预加载。优化之后打开其他页面的速度。
<link href="js/homepage.0117b45d.js" rel="prefetch">
<link href="css/app.806dd9fb.css" rel="preload" as="style">

//
preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源
prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源
在VUE SSR生成的页面中,首页的资源均使用preload,而路由对应的资源,则使用prefetch
2.js问题
  • 将不需要的首屏加载的script,加上asyncdefer,减少js阻塞渲染进程的时间
  • 如果 async="async":脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)
  • 如果不使用 async 且 defer="defer":脚本将在页面完成解析时执行
  • 如果既不使用 async 也不使用 defer:在浏览器继续解析页面之前,立即读取并执行脚本
  • 如果<Script>加载的文件不需要影响到首屏渲染的话,尽量放在body的底部,避免阻塞渲染进程,浏览器会在获取到部分DomTree、CssTree的时候合并RenderTree来进行页面渲染。
  • 对于需要引入的文件或者是页面进行按需加载、懒加载等优化
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 提取公用函数及逻辑,减少重复代码
3.httml+css问题
  • 减少页面会触发回流重绘的操作,具体可以参照这篇文章《浏览器的回流与重绘》
  • 尽量减少css选择器的层级,直接赋予id或者class进行样式的添加
  • 尽量不要用js操作css,如果一定要用的话也记得,先把读取样式的操作放在前面执行,然后再将执行添加/修改样式的操作。在涉及到dom操作的时候尽量都现将js的逻辑先写完,然后再去一次性操作dom。
  • 长列表可以采用虚拟列表进行实现,数据量过多的使用骨架屏、loading样式、分页也能优化用户体验
4.图片问题
  • JPG 有损压缩、体积小。PNG无损压缩、质量高。在不同的情况用不同的格式有利于优化图片加载速度。 在轮播大图、头像等一般使用JPG,图标、大logo的话一般使用PNG。
  • 部分内存较小的图片可用在线转换网站,将图片转换成bse64格式再进行加载,能够减少http的请求次数。
  • 将许多小的图合并至同一张图中,然后通过css进行加载,这种做法合并的图一般叫做雪碧图。可以使用在线网站css sprites tool,直接上传你需要的图标,他会帮你合并图片并且生成对应的css预览。
  • 时间足够的话,部分图片也可以考虑用canvas/svg进行实现,优化性能。
  • 图片懒加载。对还没到可视区域的图片进行隐藏,常用方法是将src设置为空,然后监听页面的滚动事件,滚动到一定距离的时候可以将图片src设置回原来的地址。

2.为什么这么卡

首先我们先使用chrome的performance对页面的性能进行解析,具体使用教程可以参照这篇文章

image.png

得到上面的图片之后,我们就可以针对占用时间较长的模块进行优化。底部的饼图有总的耗时时间,一般都是scripting和rendering占用的时间较多。

Scripting:Javascript执行
Rendering:样式计算和布局,即重排
Painting:重绘 对应的详细事件

问题分析

首先我们需要知道,一般的显示器是刷新率是 60 HZ,一个流畅的网页动画的要求就是 1 秒 60 帧,即一秒重新渲染页面 60 次,一次渲染出来的页面叫一帧,动画的本质就是帧的切换。在一次事件循环里,一个宏任务被执行后,js 修改了样式,浏览器也不一定会重新渲染,浏览器可能等到下一次事件循环再一起渲染,而中间没有渲染的那一次,就不会再被渲染出来了,这就叫 “丢帧”。

页面重新渲染间隔大于 16.67 毫秒,动画就会产生卡顿;

优化思路

  • 减少会引起回流和重绘的操作

  • 尽量使用CSS3动画,CSS3动画在大部分浏览器都开启了硬件加速,比如:在css属性上加上transform:translateZ(0)开启硬件加速

  • 减少js访问dom的次数,部分如window对象的resize,scroll事件,还有document.mousemove等事件,频繁执行dom操作,资源加载等重行为,导致UI停顿甚至浏览器奔溃。建议对函数里面需要执行的函数进行节流操作

  • 如果是使用了setTimeout 或 setInterval 函数来执行动画导致页面卡顿了的话,可能是丢帧的原因导致的,可以看看window.requestAnimationFrame() 方法,将需要用js改变样式代码统一放到下一次重新渲染时执行。

  • 优化可能会引起阻塞的js代码,看看有没有什么死循环或者时间复杂度较高的函数,优化掉

3.为什么打出来的包这么大

问题分析

93f72404-b338-11e6-92d4-9a365550a701.gif

webpack-bundle-analyzer是webpack的一款可视化工具插件

它可以直观分析打包出的文件包含哪些,大小占比如何,模块包含关系,依赖项,文件是否重复,压缩后大小如何,针对这些,我们可以进行文件分割等操作。

vue-cli创建的项目里,在package.json里的script里加上--report,然后运行,旧的版本是会直接让你打开一个地址,新的版本会在打包后的目录里生成一个report.html,打开即可。

"scripts": {
   ...
   "build": "vue-cli-service build --report",
   ...
}

其他使用webpack创建的目录可以用

npm install --save-dev webpack-bundle-analyzer

然后在webpack的plugins配置的地方引入即可

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

优化思路

关于webpack对包大小的优化,在webpack4的版本几乎都合并到optimization选项里了,可以逐个翻阅

  • 逐个检查webpack的插件,去除可能会导致包体积增加的没必要的插件,例如部分只需用于本地调试用的插件HotModuleReplacementPlugin等

  • 使用webpack的压缩代码的插件uglifyjs-webpack-plugin,或者使用在线压缩代码的网页对代码进行压缩。

  //webpack.config.js
  
  const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
  
  optimization: {
     minimizer: [new UglifyJsPlugin()],
   },

webpack4以上的话不用以上的写法,直接使用optimization.minimize,详情可以翻阅文档。

  • TreeShaking可以在打包的时候帮我们移除javascript上下文中的未引用代码。TreeShaking在webpack2已经支持了。但是旧的项目如果用的是webpack3或之前的版本,考虑升级至webpack4/5,webpack4以上的版本对TreeShaking的检测能力进行进一步的优化。

  • webpack4之前的SplitChunksPlugin以及webpack4之后的optimization.splitChunks,可以抽取页面的公用模块,避免不同页面之间的重复依赖。下面是optimization.splitChunks的示例代码。

  module.exports = {
    optimization: {
        splitChunks: {
            chunks: "async",// all async initial
            minSize: 30000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: "~",
            name: true,
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
        }
    },
   }
  • 引用例如lodash、moment等常用工具库的时候,优先考虑按需引入,或将需要使用到的部分代码,直接拷贝至项目常用的工具函数文件中,自己维护一套常用的工具库,减少包的体积。

  • 假如要引用现成的内存较大的库,比如echarts、jquery等,优先考虑CDN引入从而减少包的体积。建议是公司自己维护一套CDN服务器,以免外网服务的CDN突然下线导致服务崩溃。 给CDN地址加上dns-prefetch,可以对DNS进行预解析。尽量把预解析的代码写到页面的最开始的部分,能尽快加载

//href只需要填你需要请求的域名//+你请求的域名,如下例
<link rel="dns-prefetch" href="//code.jquery.com" />

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>

使用了CDN之后,webpack的项目还可以搭配external,设置import引入的包的名字,之后你就可以像使用npm安装的包一样直接import相对应的名字就可以使用。而且webpack打包时会不从node_modules把external中的包打包进去,从而减少打包的体积

//index.html,引入CDN服务器的Jquery

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>

//webpack.config.js,然后再webpack中设置externals

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};

//引入的时候就和npm安装的包一样,用import引入即可

import $ from 'jquery';
$('.my-element').animate(/* ... */);

  • 部分大一点的库还会将部分代码进行拆分,可以进行模块化引入。如echarts、loadsh等
import { debounce } from 'lodash'
import { throttle } from 'lodash'

// 改成如下写法,找到相对应的库的地址即可

import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle'
  • 如果项目较大的话考虑使用微前端的方式对项目进行拆分,可以参考qiankun


以上仅为本人整理的常见的优化思路,欢迎大家补充,实在不行就花钱买更好的服务器配置!重构整个项目!包治百病!看完的话麻烦点个赞啦谢谢