性能优化 - 详解

289 阅读13分钟

是什么:

提升网站性能

性能指标:

等待时间(TTFB):直观反映网络情况和后台处理能力

所有异步请求能在1秒内完成 - 及时给用户反馈 - 否则加loading动画等交互效果

动画页面效果不低于60帧每秒(FPS) - 打开F12调试工具 输入command + shift + p 输入frame或中文“帧”, 找到show frames per second FPS meter,然后回车 浏览器左上角就会出现帧数的显示

谷歌RAIL评估标准:

  1. 响应:处理事件应该在50ms内完成
  2. 动画:每10ms产生1帧
  3. 空闲:尽可能多,业务逻辑计算应放后台做,不然少空闲时间可能导致页面卡顿
  4. 加载:5s内完成所有内容加载并可交互

为什么:

从用户的角度而言,当打开一个网页,往往关心的是从输入完网页地址后到最后展现完整页面这个过程需要的时间,这个时间越短,用户体验越好。所以作为网页的开发者,就从输入url到页面渲染呈现这个过程中去提升网页的性能。

Amazon发现每100ms延迟,会导致1%销量损失。

移动端用户更缺乏耐心,>3秒的加载导致53%的用户跳出。

怎么做:

性能测试工具:

  • chrome DevTools开发调试,性能评测
  • Lighthouse网站整体质量评估
  • WebPageTest多测试地点,全面性能报告

可同步加载的资源同步加载(如图片)

压缩资源

web APIs - 有很多关键节点api提供使用和计算

DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数:performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart

一、页面加载及渲染过程优化

注意点:

浏览器渲染流程

v2-56925e49beed07b72574f88074e41074_720w.jpg

HTML文本转为DOM树:

0e90c8ef17f297d8d02bc17004a3fa6.png

CSSOM树:

8edc5f9595397a010d8dca7d4422030.png

DOM树和CSSOM树结合成Render树:

7d71ef1e2b8a7b8111feebc3646ea75.png

浏览器渲染流程(关键渲染路径):js(HTML绘制成DOM树) -> style -> layout(布局) -> paint(绘制) -> composite

  1. 解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
  2. CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树
  3. 布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的位置和大小等
  4. 绘制 RenderObject 树 (paint),绘制页面的像素信息
  5. 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面

影响回流(除第一次布局(Layout)后的其它布局(reflow))的操作:

  1. 添加/删除元素
  2. display:none
  3. 移动元素位置
  4. 操作styles
  5. offsetLeft scrollTop clientWidth
  6. 修改浏览器大小,字体大小

避免回流,布局抖动(layout thrashing):

  1. 使用css3硬件加速,可以让transform、opacity、filters等动画效果不会引起回流重绘
  2. 读写分离,如虚拟DOM那样批量处理 - 可用fastdom插件

requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:

1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。

2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。

<!doctype html>
<html lang="en">

<head>
    <title>Document</title>
    <style>
        #e {
            width: 100px;
            height: 100px;
            background: red;
            position: absolute;
            left: 0;
            top: 0;
            zoom: 1;
        }
    </style>
</head>

<body>
    <div id="e"></div>
    <script>
        var e = document.getElementById("e");
        var flag = true;
        var left = 0;
        //当前执行时间
        var nowTime = 0;
        //记录每次动画执行结束的时间
        var lastTime = Date.now();
        //我们自己定义的动画时间差值
        var diffTime = 40;

        function render() {
            if (flag == true) {
                if (left >= 100) {
                    flag = false
                }
                e.style.left = ` ${left++}px`
            } else {
                if (left <= 0) {
                    flag = true
                }
                e.style.left = ` ${left--}px`
            }
        }
        //requestAnimationFrame效果
        (function animloop() {
            //记录当前时间
            nowTime = Date.now()
            // 当前时间-上次执行时间如果大于diffTime,那么执行动画,并更新上次执行时间
            if (nowTime - lastTime > diffTime) {
                lastTime = nowTime
                render();
            }
            requestAnimationFrame(animloop);

        })()
    </script>
</body>

</html>

www.jianshu.com/p/fa5512dfb…

相同大小的js与图片等其它资源编译和执行时间是不同的

微信图片_20220122170258.png

解决:

  1. 代码拆分,按需加载
  2. 代码减重

减少主线程工作量

  1. 避免长任务
  2. 避免超过1kb的行间脚本
  3. 使用rAF和rIC进行时间调度

v8优化机制: - 待详解

  • 脚本流
  • 字节码缓存
  • 懒解析

v8优化:懒解析(lazy parsing)vs饥饿解析(eager parsing):饥饿解析多了括号封住函数,提前解析

const add = (a, b) => a + b; //lazy parsing
// const add = ((a, b) => a + b); //eager parsing
const num1 = 1;
const num2 = 2;
let addNum = add(num1, num2);
console.log(addNum);

v8对象优化:

  • 以相同顺序初始化对象类型,避免隐藏类的调整
  • 实例化后避免添加新属性
  • 尽量使用Array代替array-like
  • 避免读取超过数组的长度
  • 避免元素类型转换
//尽量使用Array代替array-like
function changeArray(...args) {
    console.log(args);
    console.log(arguments);
    console.log(Array.isArray(args)); // true
    console.log(Array.isArray(arguments)); // false

    // Array.prototype.forEach.call(arguments, (value, index) => {
    //     console.log(`${index}:${value}`);
    // });

    const arr = Array.prototype.slice.call(arguments, 0); //转为array会更快点
    arr.forEach((value, index) => {
        console.log(`${index}:${value}`);
    });
}

changeArray('a', 'b', 'c', 'd', 1);

HTML优化:

  • 减少iframes使用
  • 避免table布局
  • 避免节点深层嵌套
  • css与js尽量使用外链
  • 删除元素默认属性
  • 打包时压缩空白符和删除注释

CSS优化:(2,3需要再细学习)

  • 降低css对渲染的阻塞
  • 利用GPU进行完成动画
  • 使用contain属性
  • 使用font-display属性

html,js,css压缩与合并

  • html,css可使用html-minifier压缩,webpack有,可配置
  • js并不是必须全合并,看项目流程,如合并确实能减少请求数量和总体请求时间,但可能导致缓存的大js文件,只要修改一小处就必须整个js都要重新下载缓存,并且可能不利于首屏加载速度,不利于按需加载。- 查看webpack配置

图片优化

  • jpg - jpg非常适合色彩丰富图片、渐变色,banner
  • png - 纹理,背景透明等有较好支持,体积大,logo等小图
  • webp - 谷歌提出,比较新,兼容性也比较差

图片懒加载,原生懒加载(lazy形式兼容一般和data-src形式),一般使用第三方插件 - 具体了解和实操

<img loading="lazy" src="https://placekitten.com/426/426" alt="图片">

使用渐进式图片 - 从虚到实 - 也有第三方工具

微信图片_20220124110604.png

使用响应式图片

  • srcset+sizes-推荐
  • picture-兼容一般

srcset:

x (像素比描述)或 w (图片像素宽度描述)描述符(与图片 URL 相隔一个空格), w 描述符的加载策略是通过 sizes 属性里的声明来计算选择的

w 描述符可以简单理解为描述源图的像素大小(无关宽度还是高度,大部分情况下可以理解为宽度)。

<img srcset="http://placehold.it/2000 2000w, http://placehold.it/1500 1500w, http://placehold.it/1000 1000w, http://placehold.it/500 500w "
        sizes="(max-width: 500px) 500px,(max-width: 1000px) 1000px,(max-width: 1500px) 1500px,2000px"
        src="http://placehold.it/500/abc" />

字体优化: 使用font-face与内部的font-display font-display 确切的说不是 CSS 属性,而是专用于 @font-face 指令的描述符,它可以取如下几个值:

  • auto 。这个是 font-display 的默认值,字体的加载过程由浏览器自行决定,不过基本上和取值为 block 时的处理方式一致。
  • block 。在字体加载前,会使用备用字体渲染,但是显示为空白,使得它一直处于阻塞期,当字体加载完成之后,进入交换期,用下载下来的字体进行文本渲染。不过有些浏览器并不会无限的处于阻塞期,会有超时限制,一般在 3 秒后,如果阻塞期仍然没有加载完字体,那么直接就进入交换期,显示后备字体(而非空白),等字体下载完成之后直接替换。
  • swap 。基本上没有阻塞期,直接进入交换期,使用后备字体渲染文本,等用到的字体加载完成之后替换掉后备字体。
  • fallback 。阻塞期很短(大约100毫秒),也就是说会有大约 100 毫秒的显示空白的后备字体,然后交换期也有时限(大约 3 秒),在这段时间内如果字体加载成功了就会替换成该字体,如果没有加载成功那么后续会一直使用后备字体渲染文本。
  • optional 。与 fallback 的阻塞期一致,但是没有交换期,如果在阻塞期的 100 毫秒内字体加载完成,那么会使用该字体,否则直接使用后备字体。这个就是说指定的网络字体是可有可无的,如果加载很快那么可以显示,加载稍微慢一点就不会显示了,适合网络情况不好的时候,例如移动网络。

webpack配置: webpack4新增mode:使用默认大于配置,development和production模式,部分不合适再按需修改。 webpack.docschina.org/configurati…

tree-shaking

  • js压缩 - Terser压缩
  • mini-css-extract-plugin 压缩css
  • htmlWebpackPlugin 压缩HTML
  • 作用域提升
  • babel7优化
  • noparse
  • DllPlugin
  • 持久化缓存方案-主要文件命名:推荐使用'[contenthash:8]'来做
  • 按需加载

webpack监测和分析工具

  • Stats 分析和可视化图
  • webpack-bundle-analyzer 进行体积分析
  • speed-measure-webpack-plugin 速度分析

nginx配置

  • 使用Gzip压缩,可设置大于1k,密码级别6级(1-9级),类型等,配置后需重启nginx
  • 使用keep-alive,keepalive_timeout 0时为不keep-alive,默认的keepalive_timeout 65时表示保持连接时间为65秒,可根据项目设定,keepalive_requests 100表示此keep-alive最大支持100个请求,可根据项目设定,目的:节省了连结创建时间,但一直保持连结又会对服务器性能造成损耗。
  • nginx设置不缓存html(因为单页面应用通过1个html入口),其它文件缓存,注意:nginx默认开启Etag

GZIP最好webpack和nginx一起配置

(1)webpack不配置gzip压缩,只有nginx配置也行,即nginx会压缩文件。 (2)给nginx配置:

gzip_static on;

同时webpack也配置了gzip压缩,nginx在返回文件时首先会查找静态资源有没有相关的gz文件,若存在返回gz文件,nginx不再压缩。

若没有找到文件,nginx压缩文件,这样会加重nginx的负担。

所以同时配置nginx和webpack会好一些

同时缺点是:为了要兼容不支持gzip的浏览器,启用gzip_static模块就必须同时保留原始静态文件和gz文件。这样的话,在有大量静态文件的情况下,将会大大增加磁盘空间

微信图片_20220124161908.png

http缓存:

  • Catch-Control/Expires (catch-control:no-store(不关心是否变化,一定和服务器同步))
  • Last-Modified + If-Modified-Since
  • Etag + If-None-Match

Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。

ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。

service-worker: - 值得深层学习

  • 延长了首屏时间,但页面总加载时间减少
  • 兼容性统计为93.86%用户支持(ie,edge,opera不支持)
  • 只能在localhost和https使用

开启http2必须先开启https,http2使用nginx配置,只需要在ssl后面加http2即可,http2新增Server push,直接推送资源给客户端

http2优势: - 推荐部署,有利无害

  1. 二进制传输(http1是文本)
  2. 多路复用
  3. Server push

微信图片_20220124202037.png

是否使用SSR架构(react的ssr是next,js),主要解决

  1. 加速首屏加载
  2. 更好的SEO

图片优化: svg>iconfont>png

svg(可img的src引入形式展示)优势:

  • 保持图片能力,支持多色彩
  • 独立的矢量图形
  • XML语法,方便搜索引擎SEO和无障碍读屏软件读取

iconfont优势:

  • 多个图标-一套字体,减少获取时的请求数量的体积
  • 矢量图形,可伸缩
  • 直接通过css修改样式

使用flex-box布局:

  • 更高性能的实现方案
  • 容器有能力决定子元素的大小,顺序,对齐,间隔等。

预加载: preload:提前加载较晚出现,但对当前页面非常重要的资源 prefetch:提前加载后续路由需要的资源,优先级低

<link rel="preload" href="main.js" as="script" />
<link rel="preload" href="media.mp4" as="video" type="video/mp4" />
<link rel="preload" href="fonts/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"> // 获取跨域

<link rel="prefetch" href="xxx/images/big.jpeg" />

webpack的prefetch引入
import(
    /* webpackPrefetch: true */ "../topic"
)

www.cnblogs.com/shenjp/p/13…

预渲染的作用:

  • 大型单页应用的性能瓶颈:JS下载+解析+执行
  • SSR的主要问题:牺牲TTFB来补救First Paint;且实现复杂
  • Pre-rending 打包时提前渲染页面,没有服务端参与

可使用React-Snap做预渲染(Vue也可用)

  • 配置postbuild
  • 使用ReactDom.hydrate()
  • 内联样式,避免明显的FOUC(样式闪动)

windowing的作用: - 待详解 加载大列表,大表单的每一行严重影响性能 Lazy loading仍然会让DOM变得过大 windowing只渲染可见的行,渲染和滚动的性能都会提升

Skeleton/Placeholder的作用

  • 占位
  • 提升用户感知性能

可使用react-placeholder

  • 预置的placeholders
  • 自定义placeholder,记得与原文件同步改

节流与防抖

谷歌浏览器的waterfall的蓝线指DOM加载的时间,红线指页面所有生命和资源加载完的时间 谷歌浏览器自带网络,lighthouse等性能检测工具,lighthouse一般多测几次取平均,减少单次网络情况误差

各类型网站统一指标不可一概而论,搜索引擎首页因没什么内容大多是完美的指标,但有些电商因首页需要多加载图片文件可能会显示warmning

常见问题:

  1. 从输入url到页面加载显示完成都发生了什么? - 详细答-前端性能优化9-1
  2. 首屏加载优化?
  3. js内存管理问题

js内存管理问题:详细答-前端性能优化9-3

  • 避免意外创建全局函数
  • 避免反复运动引发大量闭包
  • 避免脱离的DOM元素

问题2:详细答-前端性能优化9-2 微信图片_20220125210003.png

微信图片_20220125211145.png

算法-还是在leecode准备

沟通-有时面试官问的也不够清楚,要问清楚再作答,沟通力也很重要 微信图片_20220125212427.png

js文件原生引入:

<script> 元素中设置 defer 属性,等于告诉浏览器立即下载,但延迟执行

HTML5 为 <script>标签定义了 async属性。与defer属性类似,都用于改变处理脚本的行为。同样,只适用于外部脚本文件。目的:不让页面等待脚本下载和执行,从而异步加载页面其他内容

把js外部引入的文件放到页面底部,来让js最后引入,从而加快页面加载速度

//脚本1
<script defer src="js/vendor/jquery.js"></script>
//脚本2
<script defer src="js/script2.js"></script>
//脚本3
<script defer src="js/script3.js"></script>

上述代码添加 defer 属性,脚本将按照在页面中出现的顺序加载,因此可确保脚本1必定加载于脚本2和 脚本3****之前,同时脚本2必定加载于脚本3之前。 (补充:最近在看《高级程序设计》里面说,实际上延迟脚本并不一定会按照顺序执行,也不一定会在DOMContenterLoaded事件触发前执行,因此最好只包含一个延迟脚本

缓存不常变更的数据: 数据缓存,可将不必实时动态加载的数据放置在vuex的state缓存中通过id为key值插入缓存的对象中,然后后续进入对应id页面,使用includes检索是否已经缓存了。