19,594

这篇文章总结一下我过往项目的web性能优化,主要是就项目中如何发现性能问题,优化如何解决,谈一下性能优化。把学习的过程跟大家分享一下,共同学习。web的性能监测工具我用的是Chrome的Performance 面板和Lighthouse。

回想起以前工作被产品拉进小黑屋,要我优化目前项目组的3个项目的性能问题。说体验起来比较卡。我带着激动地心,颤抖的手打开项目跑一下分数,普遍15-60分。因为我们项目是数据可视化的项目。追求的是什么?酷炫吊炸天!!!!各种3D的地图,酷炫的动画,复杂的交互。优化……好吧,打工人打工魂。哦豁,优化好像依然没有达到预期的效果,经过不懈的努力还是能有质的提升。因为公司项目是内网的(公司安全防范超过严格,啥也看不了),我是半行代码也拷不出来,也不敢贴出来呀,我尽量把问题制造在demo里,也方便观察和解决。

拓展插件,没有安装的安装看一下。如果有错误的地方希望大家提出来,以便我能及时修改~

一、 Lighthouse

Lighthouse生成的是一个报告,会给你的页面跑出一个分数来。 分别是页面性能(performance)、Progressive(渐进式 Web 应用)、Accessibility(可访问性)、Best Practices(最佳实践)、SEO 五项指标的跑分。甚至针对我们的性能问题给出了可行的建议、以及每一项优化操作预期会帮我们节省的时间。这份报告的可操作性是很强的——我们只需要对着 LightHouse 给出的建议,一条一条地去尝试,就可以看到自己的页面,在一秒一秒地变快。

打开项目跑一下分数,看看都有什么问题(分数每次测会有偏差,而且跟网速也有关系,所以不用太在意分数,关心一下Lighthouse给我们的建议)还有个人觉得在生产环境测会比本地靠谱

那让我们为了写个dome look look ,把问题制造出来。为了快速,我写的dome是用vuecli 4.0所以很多webpack配置是内置的,但我实际工作用的是react,无论脚手架怎么变,原理不会变。

1. Use HTPP/2

嗯,这个这个,HTTP/2的优点我就不多说了,大家可以去了解一下。15年发表到现在已经很成熟了,可以使用起来~

2. Remove unused CSS 或者 Remove unused JavaScript(移除无用的js和css)

考虑一下按需引入和CDN(这个在实际项目中会比较复杂,因为代码比较多。分析问题比较难)

我随便加了几个第三方库,写了几个页面。首先看看大小,明明打包出来压缩的是493k的怎么加载时1643k呢

去看看nginx配置,哦豁,示范的时候,我把开启Gzip注释了,放开放开,之后看看正常了。

目测打包出来1M多的chunk.js也是有点大,webpack普遍优化:切割一下打包文件,不要全部打包到一起。全局引入了echarts和element我们把这两个js分开打包, 使用optimization.splitChunks

我把压缩释放看看,点开Coverage面板,刷新看看。可以看到那些是关键资源(红色非关键,蓝色关键资源)。

点击查看详细加载情况,红色的就是没有使用的代码,但是打包压缩后的代码,我们一般都看不出来是人还是鬼。所以我们尽量对代码进行切割,不仅可以减少大文件加载的时间,也可以明确问题所在。

我们可以处理一下第三方的js,能看到echarts和elemenet-ui加载的大小和实际使用的大小有出入,一般Lighthouse用超过20 kb的未使用代码标记每个JavaScript文件。我们改变一下element-ui的引入方式,目前是全局引入,我们可以使用按需引入,因为目前我只用到了Button,我们就只需要引入Button。优化后打包大小明显变小。

如果项目使用的组件比较多,按需引入不方便,必须全局引入。这时候考虑一下,因为第三方的库我们不会经常去更新的,可以使用cdn的方式引入,就减免打包的大小。

配置(我vue也不怎么用,大家参照参考官网配置就好,注意的是vue必须要先加载了,不然element-ui会报错。如果开发环境使用**min.js可能无法调试) 使用(不再需要import,其他正常使用就好)

3.Serve static assets with an efficient cache policy(为静态资源提供缓存)

对于不常改变的静态资源比如说css、image等可以进行缓存,针对缓存也总结了一下,可以看看

打开ngnix配置文件,把缓存配上(这里粗暴把js和css缓存了,实际项目根据实际需要配置缓存)

4. Minimize main-thread work 最小化主线程工作

浏览器的渲染器过程将您的代码转换为用户可以与之交互的网页。默认情况下,渲染器进程的主线程通常处理大多数代码:它解析HTML并构建DOM,解析CSS并应用指定的样式,并解析,评估并执行JavaScript。主线程还处理用户事件。因此,每当主线程忙于执行其他操作时,您的网页就可能无法响应用户交互,从而导致不良的体验。

看看dome的例子,主要渲染时间在Style&&Layout,说明我们的重排重绘时间占了主要时间,那我们就想办法减少重排,这部分在performance面板看比较直观,下面performance会讲到这个例子的处理,这里跳过。

Google对我们的建议是:

这个会比较笼统,只能根据项目一点一点改变, 样式、布局和渲染下面performance会介绍到

  • 优化第三方JavaScript
  • 消除您的输入处理程序
  • 使用网络工作者
  • 样式和布局
  • 减少样式计算的范围和复杂性
  • 避免大型,复杂的布局和布局颠簸
  • 坚持只使用合成器属性并管理层数
  • 简化paint复杂性并减少paint面积
  • 解析HTML和CSS
  • 提取关键CSS
  • 缩小CSS
  • 推迟非关键CSS
  • 脚本解析和编译
  • 通过代码拆分减少JavaScript负载
  • 删除未使用的代码
  • 如何分析非关键代码

    上面有提过 Coverage Tool

    图片可以看出这个页面其实根本就不使用到element-ui css, 我们可以设置延迟加载非关键CSS

    webpack设置 html-critical-webpack-plugin

  • link rel="preload" as="style"异步请求样式表。您可以preload在 《预载关键资产》指南 中了解更多信息。
  • onload属性link允许CSS在加载完成后进行处理,在这里执行null转化,可以避免在切换rel属性时重复处理
  • noscript元素对不支持javascript的浏览器做兼容。
  • 5. Ensure text remains visible during webfont load 确保文本在Webfont加载期间保持可见
  • FOIT是浏览器在加载字体的时候的默认表现形式,也就是在字体加载过程中,页面是看不到文本内容的。在现代浏览器中,FOIT会导致这种现象出现至多3秒。FOIT会导致很差的用户体验,这是我们需要尽量去避免的.
  • FOUT意思是在字体加载过程中使用默认的系统字体,字体加载完后显示加载的字体,如果超过了FOIT(3s)字体还没加载,则继续使用默认的系统字体。
  • swap告诉浏览器使用字体的文本应立即使用系统字体显示。自定义字体准备好后,它将替换系统字体。可以避免在大多数现代浏览器中使用FOIT(并非所有主流浏览器都支持font-display: swap)
  • 7. Reduce JavaScript execution time 减少js的执行时间

    当JavaScript执行时间超过2秒时,Lighthouse将显示警告。执行时间超过3.5秒时,审核将失败

    建议(这些webpack都有相关的配置):

  • 拆分代码。
  • 缩小并压缩代码
  • 删除未使用的代码 (tree shaking)
  • 使用缓存代码(上面有讲)
  • 8. Avoid enormous network payloads 避免大量的网络负载

    这个涉及因素比较多,考虑从多方面入手,参考下面方法:

    减少网络负载方法

  • 将请求推迟到需要时再发送。有关的方法,请参阅PRPL模式。
  • 最小化和压缩网络负载。
  • 对图像使用WebP而不是JPEG或PNG(图片要求不严格,可以压缩体积,我常用的在线压缩网站 tinypng.com/)。
  • 将JPEG图像的压缩级别设置为85。
  • 缓存请求,以使页面在重复访问时不会重新下载资源。(请参阅“网络可靠性”登录页面,以了解缓存的工作原理以及实现方法。)
  • Push(推送或预加载)最重要的资源。
  • Render:尽快渲染初始路线。
  • Pre-cache 预缓存剩余资源。
  • Lazy load 延迟加载其他路由和非关键资源。
  • 比如:vuecli3.x or 4.x默认打包之后,部署到服务器上的项目,会对静态资源的标签上默认加载preload或者prefetch属性(preload主要用于预加载当前页面需要的资源;而prefetch主要用于加载将来页面可能需要的资源)

    预加载&&延迟加载

    preload :是一种声明式的资源获取请求方式,用于提前加载一些需要的依赖,并且不会影响页面的onload事件。通过在HTML文档的开头添加标记rel="preload"来预加载关键资源,浏览器为资源设置了更合适的优先级,如果as属性被省略,那么该请求将会当做异步请求处理:
    <link rel="preload" as="style" href="css/style.css">

    prefetch :是一种利用浏览器的空闲时间加载页面将来可能用到的资源的一种机制;通常可以用于加载非首页的其他页面所需要的资源,以便加快后续页面的首屏速度;

    延迟加载 :是一种根据需要而不是预先加载资源的策略。这种方法在初始页面加载期间释放了资源,并避免了加载从未使用过的资产。

    如果您在网页上加载许多图像,请在加载页面时推迟所有折叠以下或设备视口之外的图像(请参阅使用 lazysizes 延迟加载图像)。

    9. Eliminate render-blocking resources 消除渲染阻止资源

    在浏览器可以呈现任何内容之前,它需要将HTML标记解析为DOM树。如果遇到任何外部样式表( <link rel="stylesheet" /> )或同步JavaScript标记( <script src="main.js"></script> ),HTML解析器将暂停。 脚本和样式表都是渲染阻塞资源,这些资源会延迟FCP,从而延迟LCP。推迟使用任何非关键的JavaScript和CSS来加快网页主要内容的加载。 减少CSS阻断时间:

  • 缩小CSS: 对于Webpack:optimize-css-assets-webpack-plugin
  • 推迟非关键CSS
  • 内联关键CSS
  • 11. Defer offscreen images 延迟加载具有lazysizes的屏幕外图像

    延迟加载图片应该经常会用到,就不示范了 vue-lazyload / lazysizes.min.js

    13. Image elements do not have explicit width and height 图片设置宽高

    这个很好理解就不写案例了————

    始终在图像和视频元素上包括width和设置height尺寸属性。 以确保在浏览器开始获取图像之前在页面上分配了足够的空间。这将最大程度地减少回流和重新布局。

    <img src="puppy.jpg" width="640" height="360" alt="Puppy with balloons" />

    或者设置height:auto 自适应保真比例也可以

    CLS较差的最常见原因是:
  • 图片无尺寸
  • 没有尺寸的广告,嵌入和iframe
  • 动态注入的内容
  • Web字体导致FOIT / FOUT
  • 在更新DOM之前等待网络响应的操作
  • 二、 Performance(当页面卡顿、慢时可以使用Performance来分析问题)

    lighthouse生成一个报告有些参数来源于performance,相对比lighthouse的分数和建议,performance用于记录和分析我们的应用在运行时的所有活动。它呈现的数据具有实时性、多维度的特点,可以帮助我们很好地定位性能问题。

    比如下图,我们看到style/Layout耗时非常夸张,但是我定位不到准确的地方,这个时候我们用performance面板看看 Performance整体分析

    想了解Performance呈现的结果,需要知道浏览器的渲染知识,这里大概罗列一下(懂的可以跳过,直接看后面案例分析)。
    1. Performance指标值
    名词 解析 详细
    FP (First Paint) 首次绘制 标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点
    FCP (First Contentful Paint) 首次内容绘制 标记浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、非空白canvas或SVG 甚至 元素.
    LCP (Largest Contentful Paint) 最大内容渲染 代表在viewport中最大的页面元素加载的时间. LCP的数据会通过PerformanceEntry对象记录, 每次出现更大的内容渲染, 则会产生一个新的PerformanceEntry对象.
    DCL (Dom Content loaded) 当 HTML文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载
    FMP(First Meaningful Paint) 首次有效绘制
    L (onLoad) 加载完成 当依赖的资源, 全部加载完毕之后才会触发.
    TTI (Time to Interactive) 可交互时间 指标用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点.
    TBT (Total Blocking Time) 页面阻塞总时长 TBT汇总所有加载过程中阻塞用户操作的时长,在FCP和TTI之间任何long task中阻塞部分都会被汇总
    FID (First Input Delay) 首次输入延迟 指标衡量的是从用户首次与您的网站进行交互(即当他们单击链接,点击按钮等)到浏览器实际能够访问之间的时间
    CLS (Cumulative Layout Shift) 累积布局偏 总结起来就是一个元素初始时和其hidden之间的任何时间如果元素偏移了, 则会被计算进去。具体的计算方法可看这篇文章 《Cumulative Layout Shift (CLS)》
    SI (Speed Index) 指标用于显示页面可见部分的显示速度, 单位是时间

    2. RAIL 性能模型

    名词 解析 详细
    response 响应 用户输入之后是否能在100ms之内响应
    这里的输入包括点击按钮、切换表单控件等,但不包括触摸滑动或滚动(50ms内完成较好)
    animation 动画 最近手机圈很流行将屏幕刷新率提升为90hz,这里hz就是帧率,90hz就是每秒有90帧,一帧就是一个画面。每秒看到的画面越多,我们就会感到越流畅,(每10ms内产生一帧较好)
    idle 浏览器空置状态 利用空闲的时间完成一些推迟的工作。推迟的工作应分为50ms的多个块进行。(尽可能增加空闲时间)
    load 加载 5s加载完成并且可以交互

    3.Performancem面板参数

    【第3以下配置都是用来模拟手机、慢网络下使用的】

    名词 解析
    no recordings 就是每一次的检测报告,可以根据每一次的检测报告,去进行性能优化的对比
    Screenshots 是用来查看在每个时间段界面的变化
    Memory 存储调用栈的大小,在不同时间段的不同大小;
    Disable Javascript samples 禁用 javascript 调用栈,关闭javaScript样本减少在手机运行时的开销,模拟手机运行时勾选
    Enable advanced paint instrumentation (slow) 记录渲染事件的细节,选择frames中的一块,可以看到区域四多了个Layers
    Network 网络模拟,可以模拟在3G,4G等网络条件下运行页面;
    CPU 用来查看电脑的性能问题,主要为了模拟底CPU下运行性能
    HEAP JavaScrip 执行的时间分布。
    区域2:网页性能总览图(overview)
    名词 解析
    FPS 每秒帧数,是用来分析动画的一个主要性能指标,对于动画而言标准是保持在60FPS。绿色越高越好,出现红色则表示FPS低(这就是你为啥觉得页面卡顿了),你可以在区域三Frames中看到具体的FPS值(见下面第二图)
    CPU 处理各个任务花费的时间,选择一段CPU统计可以在区域四的Summary看到统计表格
    Scripting 脚本
    Rendering 渲染
    Painting 绘制
    Loading 加载
    ldle 闲置
    NET 每条彩色横杠表示一种资源。横杠越长,检索资源所需的时间越长。 每个横杠的浅色部分表示等待时间(从请求资源到第一个字节下载完成的时间)。
    区域3:线程面板
    名词 解析
    Frames 帧线程,鼠标悬浮绿色块可以看到fps
    Main 主线程,负责执行Javascript, 解析HTML/CSS, 完成绘制。
    可以看到主线程调用栈和耗时情况,每个长条都是一个事件,悬浮可以看到耗时和事件名
    x轴指时间: 最上面的第一条就是事件触发的地方,直到结束,这条线是最长的
    y轴指调用栈:上面的event调用了下面的子event,越到下面数量越少(瀑布)
    Raster Raster线程,负责完成某个layer或者某些块(tile)的绘制。光栅化线程池,用来让 GPU执行光栅化的任务
    Interactions 用来记录用户交互操作,比如点击鼠标、输入文字、动画等
    Timings 用来记录一些关键的时间节点在何时产生的数据信息,诸如 FP、FCP、LCP 等
    Compositor 合成线程的执行记录,用来记录html绘制阶段 (Paint)结束后的图层合成操作

    区域4:统计面板

    名词 解析
    Summary 统计图:展示各个事件阶段耗费的时间
    Bottom-Up 排序:可以看到各个事件消耗时间排序
    (1)self-time 指除去子事件这个事件本身消耗的时间
    (2)total-time 这个事件从开始到结束消耗的时间(包含子事件)
    Call Tree 调用栈:Main选择一个事件,表示事件调用顺序列表(从最顶层到最底层,而不是只有当前事件)
    Event Log 事件日志
    (1) 多了个start time,指事件在多少毫秒开始触发的
    (2) 右边有事件描述信息
    下面举两个例子来了解一下,怎么找出性能问题,并且解决

    1、下面这张图,动画看起来是有点卡顿的问题的,利用performance测试一下,可以看到Main面板里面展开看到很多Task右上角都有红色的三角形,说明是个Long Task,注意Animation Frame Fired事件右上角的红色三角形图标,该红色三角图标是这个事件有问题的警告。

    选中其中一个Animation Frame Fired 查看详细信息,可以看到有问题的代码在第几行,点击代码会跳到指定代码

    再放大可以看到Animation Frame Fired有很多紫色的Layout模块,右上角也有红色三角形。点击选中Forced reflow查看信息,看到造成卡顿的原因是很多元素进行不断的回流和重绘。

    看一下代码,有1000个元素在不断通过他的offsetTop改变宽(如果你电脑性能比较好,可以改成5000个,卡顿效果更佳明显)。当你要用到像这样的属性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 时,需要通过即时计算得到,所以也进行回流(重排)。我们无法避免读取属性,但是不用每一个元素读取完offsetTop再马上赋值,我们可以先批量读取完属性后再赋值.可以利用开源的 fastdom 来解决这个问题(可以去看看它的源码,没有多少行)

    2.使用fastdom优化的代码(读写分离)

    3.优化前后效果对比

    再举个简单,无伤大雅的例子

    下面是需要拖拽线条位置改变方格大小(当然了,只写了线条拖拽功能),再performance上看到有很多layout Shift(布局偏移)

    放大Mian

    看代码,利用定位改变left的位置进行移动的,这样会造成重排。我们可以利用transform代替,减少重排。

    参考文献:

    Google 的 web.dev/