网页性能优化系统性总结

454 阅读13分钟

image.png

性能优化指标:FMP LCP FP FCP CLS TTI INP

1.FMP(first meaningful paint 首次有效绘制)

指页面的首要内容(primary content)出现在屏幕上的时间,这又涉及到一个关键---首屏关键CSS(critical CSS),这个时间的缩短,可以有效提高FMP。

2.LCP

如何加快首屏关键CSS

  • 合理的书写内联样式 [这里的意思就是在html文件中,在style标签中书写一些关键的CSS样式,但是一般大小需要控制在14kb左右] 【Github 上有一个项目Critical CSS】可以帮助去提取关键CSS!!!

内联 CSS 有一个缺点,内联之后的 CSS 不会进行缓存,每次都会重新下载,如上所说,如果我们将内联后的文件大小控制在了 14.6kb 以内,这似乎并不是什么大问题。

2.异步加载CSS

  • js 动态创建link 标签
// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );

  • 修改link 的某些属性,最后加载完后动态修改属性
第二种方式是将 link 元素的`media`属性设置为用户浏览器不匹配的媒体类型(或媒体查询)

如`media="print"`,甚至可以是完全不存在的类型`media="noexist"`。对浏览器来说,如果样式表不适用于当前媒体类型,其优先级会被放低,会在不阻塞页面渲染的情况下再进行下载。

当然,这么做  只是为了实现 CSS 的异步加载, 别忘了在文件加载完成之后,将`media`的值设为`screen``all`,从而让浏览器开始解析 CSS。

<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">

与第二种方式相似,我们还可以通过`rel`属性将`link`元素标记为`alternate`可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将`rel`改回去。

<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">

  • [rel="preload"] 属性

<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">

注意,`as`是必须的。忽略`as`属性,或者错误的`as`属性会使`preload`等同于`XHR`请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。`as`的可选值可以参考上述标准文档。

看起来,`rel="preload"`的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载 CSS 文件但不解析,直到加载完成并将修改还原,然后开始解析。

但是它们之间其实有一个很重要的不同点,那就是**使用 preload,比使用不匹配的`media`方法能够更早地开始加载 CSS**。所以尽管这一标准的支持度还不完善,仍建议优先使用该方法。

3.文件压缩,也就是所谓的工程化打包

4. 去除无用的CSS,延迟加载首屏内容为用到的CSS【webpack 使用html-critical-webpack-plugin】

1.手动删除,一般适用于小项目 2.大项目可以接注意 uncss[ ](uncss/uncss: Remove unused styles from CSS (github.com))

5.高效使用选择器

  • 保持简单,不要使用嵌套过多过于复杂的选择器。

  • 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。

  • 不要使用类选择器和 ID 选择器修饰元素标签,如h3#markdown-content,这样多此一举,还会降低效率。

  • 不要为了追求速度而放弃可读性与可维护性。

Tips: CSS选择器 匹配是从右向左匹配的

6. 减少使用昂贵的属性

在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写 CSS 时,我们应该尽量减少使用昂贵属性,如box-shadow/border-radius/filter/透明度/:nth-child等。

当然,并不是让大家不要使用这些属性,因为这些应该都是我们经常使用的属性。之所以提这一点,是让大家对此有一个了解。当有两种方案可以选择的时候,可以优先选择没有昂贵属性或昂贵属性更少的方案,如果每次都这样的选择,网站的性能会在不知不觉中得到一定的提升。

7. 优化重排与重绘

在网站的使用过程中,某些操作会导致样式的改变,这时浏览器需要检测这些改变并重新渲染,其中有些操作所耗费的性能更多。我们都知道,当 FPS 为 60 时,用户使用网站时才会感到流畅。这也就是说,我们需要在 16.67ms 内完成每次渲染相关的所有操作,所以我们要尽量减少耗费更多的操作。

7.1 减少重排

重排会导致浏览器重新计算整个文档,重新构建渲染树,这一过程会降低浏览器的渲染速度。如下所示,有很多操作会触发重排,我们应该避免频繁触发这些操作。

  1. 改变font-sizefont-family
  2. 改变元素的内外边距
  3. 通过 JS 改变 CSS 类
  4. 通过 JS 获取 DOM 元素的位置相关属性(如 width/height/left 等)
  5. CSS 伪类激活
  6. 滚动滚动条或者改变窗口大小

此外,我们还可以通过CSS Trigger15查询哪些属性会触发重排与重绘。

值得一提的是,某些 CSS 属性具有更好的重排性能。如使用Flex时,比使用inline-blockfloat时重排更快,所以在布局时可以优先考虑Flex

7.2 避免不必要的重绘

当元素的外观(如 color,background,visibility 等属性)发生改变时,会触发重绘。在网站的使用过程中,重绘是无法避免的。不过,浏览器对此做了优化,它会将多次的重排、重绘操作合并为一次执行。不过我们仍需要避免不必要的重绘,如页面滚动时触发的 hover 事件,可以在滚动的时候禁用 hover 事件,这样页面在滚动时会更加流畅。

此外,我们编写的 CSS 中动画相关的代码越来越多,我们已经习惯于使用动画来提升用户体验。我们在编写动画时,也应当参考上述内容,减少重绘重排的触发。除此之外我们还可以通过硬件加速16和will-change17来提升动画性能,本文不对此展开详细介绍,感兴趣的小伙伴可以点击链接进行查看。

最后需要注意的是,用户的设备可能并没有想象中的那么好,至少不会有我们的开发机器那么好。我们可以借助 Chrome 的开发者工具进行 CPU 降速,然后再进行相关的测试,降速方法如下图所示。

如何开启Chrome的CPU降速

如何开启Chrome的CPU降速

如果需要在移动端访问的,最好将速度限制更低,因为移动端的性能往往更差。

8. 不要使用@import

最后提一下,不要使用@import 引入 CSS,相信大家也很少使用。

不建议使用@import 主要有以下两点原因。

首先,使用@import 引入 CSS 会影响浏览器的并行下载。使用@import 引用的 CSS 文件只有在引用它的那个 css 文件被下载、解析之后,浏览器才会知道还有另外一个 css 需要下载,这时才去下载,然后下载后开始解析、构建 render tree 等一系列操作。这就导致浏览器无法并行下载所需的样式文件。

其次,多个@import 会导致下载顺序紊乱。在 IE 中,@import 会引发资源文件的下载顺序被打乱,即排列在@import 后面的 js 文件先于@import 下载,并且打乱甚至破坏@import 自身的并行下载

所以不要使用这一方法,使用 link 标签就行了。

工程化方案:

9.借助于工具排查

开发者工具 LightHouse

报告检测:

image.png

image.png

这里面会指出一些问题,包含那些可以优化的点。点击展开会有对应的提示。 比如:启动文本压缩,会提示:文本资源开启 gzip deflate,压缩后在传输,会降低传输的成本

image.png

由此可以得出两个重要的优化措施:

1.nginx开启Gzip,一般都用nginx,前端打包的时候也使用相应的插件

npm install CompressionPlugin --save-dev

vue.config.js/webpack.config.js中配置
  plugins:[
    //  new unocss(),
    new CompressionPlugin({
      filename: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp('\\.(' + ['html', 'js', 'css'].join('|') + ')$'),
      threshold: 10240,
      minRatio: 0.8,
        deleteOriginalAssets: false, // 删除原文件
    }),
    ]

2.vite或者webpack打包压缩代码

【在查阅很多别人的经验后,我发现最有效的方式还是将打包优化后,体积还是很大的包,通过external字段,排除在打包之外,然后通过cdn的方式去引入。内网环境就自己打一个文件服务器系统,外网的话就挑选比较稳定的cdn服务器 比如字节的,或者腾讯 ali的 或者bootcdn 也可以】

在接着阅读,发现几个不是一定会用到:

3.图片格式的最佳选择,WebP 和 AVIF 等图片格式的压缩效果通常优于 PNG 或 JPEG,因而下载速度更快,消耗的数据流量更少。详细了解现代图片格式

4.图片大小的选择,对于一些logo图片,一定要叫ui 提供合适的大小,这样在项目中万一积少成多,加载性能也是有的

5.减少http请求 适当合并合并样式表和js文件。以减少http的次数,http建立是需要消耗性能的; 减少请求次数:合并和减少静态资源的数量,从而减少页面加载时的HTTP请求次数,从而提高页面加载速度(精灵图、BFF架构模式在BFF层对接口进行聚合、合理利用本地缓存–将一些频繁请求且内容不变化的数据使用本地缓存)

比如多个图片资源,可以把它们组合成一张图,也就是精灵图的方式。通过控制background-image background-position来显示图片。 还可以使用图片地图的方式来实现,也是将五张图片合成一张,然后通过map area 来实现

-   <img usemap=”#map1″ border=0 src=””>
-   <map name=”map1″>
-       <area shape=”rect” coords=”0,0,31,31″ href=”javascript:alert(‘Home’)” title=”Home”>
-       <area shape=”rect” coords=”36,0,66,31″ href=”javascript:alert(‘Gifts’)” title=”Gifts”>
-       <area shape=”rect” coords=”71,0,101,31″ href=”javascript:alert(‘Cart’)” title=”Cart”>
-       <area shape=”rect” coords=”106,0,136,31″ href=”javascript:alert(‘Settings’)” title=”Settings”>
-       <area shape=”rect” coords=”141,0,171,31″ href=”javascript:alert(‘Help’)” title=”Help”>
-   </map>

6.查看代码覆盖率【可以看到代码未使用字节数】

image.png 可以看到 element-plus 未使用的字节数高达 八九十,后续还有 一些库都是类似的去排查。

然后将这些字节使用度不高的插件有全局引入变成按需引入!!!,结合这个覆盖范围工具栏在配合 打包插件: webpack:webapck-bundle-analyze vite:rollup--plugin-visualize 这些打包可视化分析工具,可以看出那些查看和包是使用率极其少的,可以寻求体积更小的解决方案或者按需加载。不要全局一次性引入整个包。

7.拆分较大的包,比如常用的echarts 和element-plus,不要打包到一起,拆开打包。

image.png


[vite,build>rollupOptions>output>manualChunks下]

build: {
      minify: 'terser',
      outDir: env.VITE_OUT_DIR || 'dist',
      sourcemap: env.VITE_SOURCEMAP === 'true' ? 'inline' : false,
      // brotliSize: false,
      terserOptions: {
        compress: {
          drop_debugger: env.VITE_DROP_DEBUGGER === 'true',
          drop_console: env.VITE_DROP_CONSOLE === 'true'
        }
      },
      rollupOptions: {
        output: {
          manualChunks: {
            echarts: ['echarts'] // 将 echarts 单独打包,参考 https://gitee.co

8.合理利用缓存. 对于某些预知不怎么会变得资源,应当设置缓存,对于常变得的资源跟接口,可以加上时间戳或者任意哈希或者请求拦截后添加相应的headers

首先浏览器在请求资源的时候,会先检查强缓存,后检查协商缓存,如果经过强缓存后是有效的,那么就会直接拿缓存中的内容。 协商缓存:如果直接在地址栏请求一个已经缓存的资源,或者cache-control为 no-cache的资源,如果这个资源没有发生任何的变动,那么服务端通常返回304,浏览器接收到304code后,会直接使用缓存中的内容


 #缓存设置相关start
            location / {
                root   html;
                index  index.html;
                expires -1s;
            }
            #设置所有yml、HTML类型文件不缓存
            location ~ .*.(yml|html)$ {
                add_header Cache-Control no-store;
            }
            #缓存设置相关end

强缓存:

image.png

expires字段:http 请求头,表示资源的绝对过期时间,参照于操作系统的日期,如果资源被缓存后,如果再次请求该资源,在有效期内,那么直接会使用缓存中的资源,状态码是200.不会真的发起http请求去下载。缺点就是会参照操作系统的时间,这个系统时间是可以人为修改的,就导致不是很有保障。 max-age:有效时间,单位,秒,优先级大于expires 1.cache-control: max-age=xxxx,public 客户端和代理服务器都可以缓存该资源; 客户端在xxx秒的有效期内,如果有请求该资源的需求的话就直接读取缓存,statu code:200 ,如果用户做了刷新操作,就向服务器发起http请求 2.cache-control: max-age=xxxx,private 只让客户端可以缓存该资源;代理服务器不缓存 客户端在xxx秒内直接读取缓存,statu code:200 3.cache-control: max-age=xxxx,immutable 客户端在xxx秒的有效期内,如果有请求该资源的需求的话就直接读取缓存,statu code:200 ,即使用户做了刷新操作,也不向服务器发起http请求 4.cache-control: no-cache, 并不是不缓存,而是要根据服务器返回的内容来决定用不用缓存 跳过设置强缓存,但是不妨碍设置协商缓存;一般如果你做了强缓存,只有在强缓存失效了才走协商缓存的,设置了no-cache就不会走强缓存了,每次请求都回询问服务端当前资源有没有变化,没有就直接返回一个带有强缓存指令的相应,有的话就返回更新后的资源 5.cache-control: no-store 不缓存,这个会让客户端、服务器都不缓存,也就没有所谓的强缓存、协商缓存了。

协商缓存:

  • etag:服务器文件资源的唯一标识符。 通常response响应头头请求都会带上etag 和last-modified,当刷新重新请求这个资源的时候,etag会被if-none-match 代替, last-modified 会被if-modified-since 代替,然后当做请求头发送给服务端,服务器比对etag 和 if-none-match,再决定返回200 还是304,304直接走缓存,200就表示服务器资源发生的改变,需要重新缓存新的资源,并且应用新的资源
  • if-none-match: 同时存在 if-none-match 以及 if-modify-since时,服务器会优先if-none-match
  • last-modified:资源最后一次更新的时间
  • if-modified-since

image.png 【在地址栏直接输入访问一个文件比如 127.0.0.1/a.js ,这种情况下强缓存其实会失效,就算在有效期也还是会发送请求给后端下载该文件,但是协商缓存还是有效的】

刷新对缓存的影响:

image.png

【硬刷新,直接跳过强缓存以及协商缓存,从服务器拿最新的。直接刷新,会跳过强缓存,但是会检查协商缓存】【对于spa,最好是对index.html 采用协商缓存,除非返回304再拿浏览器的缓存】

image.png

image.png

  1. 延迟加载非关键css以及js

image.png

  1. tree shaking 【删除未使用的代码】

11.预加载&&延迟加载

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

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

延迟加载 :是一种根据需要而不是预先加载资源的策略。这种方法在初始页面加载期间释放了资源,并避免了加载从未使用过的资产。prefetch,多个prefetch时,还可以通过webpack 设置webapckPrefetch 来设置加载的优先级,越大越先加载。

性能工具: performance

image.png

1xx:细枝末节

  • 对于字体,使用 font-display:swap, 来告诉浏览器字体文本立即使用系统字体,等自定义字体准备好再替换。可以避免FOIT。

  • 图片懒加载

  • 使用 transform 代替left offsetLeft 等定位

  • 使用到的库,如有更新,一般都更新一下,因为有些老旧的包,旧的包不支持按需引入或者tree shaking,新版本可能就支持了

  • 异步加载,首屏资源体积对于首屏体验是很重要的,对于一些体积大但是又不是需要立即就需要的功能可以做异步加载或者动态加载

  • 骨架屏,减少用户等待的焦虑,先让用户看到点画面先。

  • 使用事件委托:将事件处理程序绑定在父元素上,利用事件冒泡机制来处理子元素的事件,可以减少事件处理程序的数量,提高性能。

  • 对于一些动画而言,需要改变元素的尺寸信息,会造成页面的回流,所以考虑将添加动画的元素脱离文档流,浮动定位或者单独抽离成一个单独的层,触发gpu硬件加速,比如transform或者opacity属性;

  • DOM元素过多会增加页面渲染的复杂性和开销。 尽量减少不必要的DOM元素,保持页面的简洁性和轻量性。遵循以下几点可以帮助减少DOM元素:

  1. 合理的页面结构设计: 设计清晰的页面结构,避免多层嵌套和复杂的布局。
  2. 使用列表渲染: 写小程序时,对于列表数据,使用小程序的列表渲染功能,减少重复的DOM元素。
  3. 合并dom操作,文档片段(DocumentFragment)等方式来减少DOM操作次数
  • 使用requestAnimationFrame 在进行复杂DOM操作时,使用requestAnimationFrame来进行渲染,可以让浏览器在下一次重绘前执行操作,提高性能

image.png

  • 除非是有必要,尽量不要为了很少的代码逻辑去全局引入一个库。
  • 减少dom渲染数,最典型的做法就是按需加载,懒加载,或者数据分页