# 性能优化面试题
# vue 性能优化
- 引入生产环境的 Vue 文件
- v-if 和 v-show 选择调用,频繁切换的使用 v-show,不频繁切换的使用 v-if
- 为 item 设置唯一 key 值
- 减少 watch 的数据,watch 对象的时候使用对象字符串
- 不要在模板里面写过多表达式
- 尽量减少 data 中的数据,data 中的数据都会增加getter 和 setter,会收集对应的watcher
- 不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)
- 对象层级不要过深,否则性能就会差
- 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
- SPA 页面适当采用 keep-alive 缓存组件
- 细分 vuejs 组件
- 内容类系统的图片资源按需加载(v-lazy、滚动到可视区域加载等)
- 单独添加的监听事件是不会移除的,需要手动移除事件的监听,以免造成内存泄漏
- 长列表性能优化(在大量数据展示的情况下,禁止 Vue 来劫持我们的数据能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据?通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了)
- 扁平化 Store 数据结构,合理使用持久化 Store 数据
- 使用路由懒加载、异步组件
- 预渲染
- SSR(服务端渲染)
# react 性能优化
这里只说 react 单独的进行的性能优化:
- key
- shouldComponentUpdate
- pureComponent
- 关于箭头函数,先声明好事件监听函数后,然后再拿到其引用传给组件:
- useCallback(大计算量的函数来)
- useMemo
- React.Memo
- 不可变数据 Immutable
- reselect
- React.lazy 按需加载
如果一定要做性能优化,核心还是在减少频繁计算和渲染上,在实现策略上主要有三种方式:利用key维持组件结构稳定性、优化数据比对过程和按需加载。其中优化数据比对过程可以根据具体使用的场景,分别使用缓存数据或组件、改用Immutable不可变数据等方式进行。最后,也一定记得要采用测试工具进行前后性能对比,来保障优化工作的有效性。
React性能优化小贴士 (opens new window)
# webpack 性能优化之加快构建速度(打包速度)
- 使用 speed-measure-webpack-plugin 插件可以测量各个插件和loader所花费的时间,量化打包速度,判断优化效果
- 通过 exclude、include 配置来确保转译尽可能少的文件
- 在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中
- 使用 happypack 开启多进程打包
- 除了使用 Happypack 外,我们也可以使用 thread-loader 开启多进程打包 loader
- 使用 webpack-parallel-uglify-plugin 开启 JS 多进程压缩,webpack 内置默认使用 TerserWebpackPlugin
- 使用 HardSourceWebpackPlugin 为模块提供中间缓存,第二次构建可大量节约时间
- 使用 noParse 来标识第三方模块没有 AMD/CommonJS 规范的模块,这样 Webpack 会引入这些模块,但是不进行转化和解析,从而提升 Webpack 的构建性能
- 使用 resolve 配置 webpack 在哪寻找模块所对应的文件
- 使用 IgnorePlugin 忽略第三方包指定目录,例如 moment 的本地语言包
# webpack 性能优化之减少打包文件体积
- 引入 webpack-bundle-analyzer 分析打包后的文件
- 使用 externals 将 JS 文件、CSS 文件和存储在 CDN
- 使用 DllPlugin(动态链接库)将 bundles 拆分,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
- 使用 optimization.splitChunks 配置抽离公共代码
- 使用 IgnorePlugin 忽略第三方包指定目录,例如 moment 的本地语言包(重复)
- 使用 url-loader 或 image-webpack-loader 对图片进行转化或者压缩处理
- 优化 SourceMap,开发环境推荐: cheap-module-eval-source-map,生产环境推荐: cheap-module-source-map
- webpack自身的优化:
- tree-shaking,在生产环境下,会自动移除没有使用到的代码
- scope hosting 作用域提升,变量提升,可以减少一些变量声明
- babel 配置的优化,配置 @babel/plugin-transform-runtime,重复使用 Babel 注入的帮助程序,以节省代码大小的插件。
# 说一下平时项目是怎么优化的?
网页从加载到呈现会经历一系列过程,针对每个过程进行优化:
- 网络连接
- 请求优化
- 响应优化
- 浏览器渲染
网络连接方面优化
- 避免重定向
- DNS 查找优化:页面采用预解析 dns-prefetch ,同时将同类型的资源放到一起,减少 domain 数量也是可以减少 DNS 查找
- 使用 CDN(内容分发网络)
- HTTP/1.1 版本,客户端可以通过 Keep-Alive 选项和服务器建立长连接,让多个资源通过一个 TCP 连接传输
请求方面优化
减少浏览器向浏览器发送的请求数目以及请求资源的大小是请求优化的核心思想
合理使用文件的压缩和合并
- 合理运用浏览器对于资源并行加载的特性,在资源的加载的数量和资源的大小之间做一个合理的平衡
- 在移动端页面中,将首屏的请求资源控制在 5 个以内,每个资源在 Gzip 之后的大小控制在 28.5KB 之内,可以显著的提升首屏时间。
压缩图片,使用雪碧图,小图片使用 Base64 内联
组件延迟加载
给 Cookie 瘦身
- 静态资源使用 CDN 等方式放在和当前域不同的域上,以避免请求静态资源时携带 Cookie
善用 CDN 提升浏览器资源加载能力
- 资源分散到多个不同的 CDN 中,最大化的利用浏览器的并行加载能力
合理运用缓存策略缓存静态资源,Ajax 响应等
- 利用 Manifest + 本地存储做持久化缓存
- 将对访问实时性要求不高的其他资源,如图片、广告脚本等内容存放在 IndexDB 或 WebSQL 中,IndexDB 后 WebSQL 的存储容量比 LocalStorage 大得多,可以用来存放这些资源。
- 使用 localForage 操作持久化缓存
- 库文件放入 CDN 或者开启强缓
响应优化
- 优化服务端处理流程,如使用缓存、优化数据库查询、减少查询次数等
- 优化响应资源的大小,如对响应的资源开启 Gzip 压缩等。
页面加载的核心指标:
TTFB
- 首个字节
FP
- 首次绘制,只有 div 跟节点,对应 vue 生命周期的 created
FCP
- 首次有内容的绘制,页面的基本框架,但是没有数据内容,对应 vue 生命周期的 mounted
FMP
- 首次有意义的绘制,包含所有元素和数据内容,对应 vue 生命周期的 updated
TTI
- 首次能交互时间
Long Task
=50ms 的任务
SSR&CSR
- 服务端渲染和客户端渲染
Isomorphic javascript
- 同构化
浏览器渲染优化
- 页面直出:骨架屏或者 SSR
- 首帧渲染优化
- 资源动态加载
- 浏览器缓存
- 优化 JavaScript 脚本执行时间
- 减少重排重绘
- 硬件加速提升动画性能等页面渲染方面的优化方案
# 优化之后是怎么度量的?首屏时间是怎么计算的?首屏优化方案?
监控方式:
以 GA(Google Analytics) 为代表的代码监控和以 WebPageTest 为代表的工具监控
关键指标:
- 白屏时间:从浏览器输入地址并回车后到页面开始有内容的时间;
- 首屏时间:从浏览器输入地址并回车后到首屏内容渲染完毕的时间;
- 用户可操作时间节点:domready 触发节点,点击事件有反应;
- 总下载时间:window.onload 的触发节点。
白屏时间
在 html 文档的 head 中所有的静态资源以及内嵌脚本/样式之前记录一个时间点,在 head 最底部记录另一个时间点,两者的差值作为白屏时间
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>白屏时间</title>
<script>
// 开始时间
window.pageStartTime = Date.now();
</script>
<link rel="stylesheet" href="">
<link rel="stylesheet" href="">
<script>
// 白屏结束时间
window.firstPaint = Date.now()
// 白屏时间 = firstPaint - pageStartTime
</script>
</head>
<body>
<div>123</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
首屏时间
由于浏览器解析 HTML 是按照顺序解析的,当解析到某个元素的时候,觉得首屏完成了,就在此元素后面加入 script 计算首屏完成时间
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首屏时间</title>
<script>
// 开始时间
window.pageStartTime = Date.now();
</script>
<link rel="stylesheet" href="">
<link rel="stylesheet" href="">
</head>
<body>
<div>123</div>
<div>456</div>
// 首屏可见内容
<script>
// 首屏结束时间
window.firstPaint = Date.now();
// 首屏时间 = firstPaint - pageStartTime
</script>
// 首屏不可见内容
<div class=" "></div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
通过 performance.timing API,可以获取各个阶段的执行时间:
window.performance
{
navigationStart: 1578537857229; //上一个文档卸载(unload)结束时的时间戳
unloadEventStart: 1578537857497; //表征了unload事件抛出时的时间戳
unloadEventEnd: 1578537857497; //表征了unload事件处理完成时的时间戳
redirectStart: 0; // 重定向开始时的时间戳
redirectEnd: 0; //重定向完成时的时间戳
fetchStart: 1578537857232; //准备好HTTP请求来获取(fetch)文档的时间戳
domainLookupStart: 1578537857232; //域名查询开始的时间戳
domainLookupEnd: 1578537857232; //域名查询结束的时间戳
connectStart: 1578537857232; //HTTP请求开始向服务器发送时的时间戳
connectEnd: 1578537857232; //浏览器与服务器之间的连接建立时的时间戳
secureConnectionStart: 0; //安全链接的握手时的U时间戳
requestStart: 1578537857253; //HTTP请求(或从本地缓存读取)时的时间戳
responseStart: 1578537857491; //服务器收到(或从本地缓存读取)第一个字节时的时间戳。
responseEnd: 1578537857493; //响应结束
domLoading: 1578537857504; //DOM结构开始解析时的时间戳
domInteractive: 1578537858118; //DOM结构结束解析、开始加载内嵌资源时的时间戳
domContentLoadedEventStart: 1578537858118; //DOMContentLoaded 事件开始时间戳
domContentLoadedEventEnd: 1578537858118; //当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳
domComplete: 1578537858492; //当前文档解析完成的时间戳
loadEventStart: 1578537858492; //load事件被发送时的时间戳
loadEventEnd: 1578537858494; //当load事件结束时的时间戳
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
资源体积太大?
资源压缩,传输压缩,代码拆分,Tree shaking,HTTP2,CDN,缓存
首页内容太多?
路由/组件/内容 lazy-loading,预渲染/SSR,Inline CSS
加载顺序不合适?
prefetch, preload
其它:cookie 瘦身,
# js是怎样管理内存的?什么情况会造成内存泄漏?怎么避免?
js是怎样管理内存的
而对于JavaScript来说,会在创建变量(对象,字符串等)时分配内存,并且在不再使用它们时“自动”释放内存,这个自动释放内存的过程称为垃圾回收。不幸的是,即使不考虑垃圾回收对性能的影响,目前最新的垃圾回收算法,也无法智能回收所有的极端情况,所以会在一些情况下导致内存泄漏。
垃圾回收算法:
- 引用计数垃圾收集
- 标记清除算法
引用计数:给一个变量赋值引用类型,则该对象的引用次数+1,如果这个变量变成了其他值,那么该对象的引用次数-1,垃圾回收器会回收引用次数为0的对象。但是当对象循环引用时,会导致引用次数永远无法归零,造成内存无法释放。
标记清除:垃圾收集器先给内存中所有对象加上标记,然后从根节点开始遍历,去掉被引用的对象和运行环境中对象的标记,剩下的被标记的对象就是无法访问的等待回收的对象。
什么情况会造成内存泄漏
- 意外的全局变量
function foo() {
bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
this.bar2 = 'some text' // 全局变量 => window.bar2
}
foo();
2
3
4
5
在这个例子中,意外的创建了两个全局变量 bar1 和 bar2
- 被遗忘的定时器和回调函数
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); // 每 5 秒调用一次
2
3
4
5
6
7
如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收,定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。
- 闭包
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // 对于 'originalThing'的引用
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。同时 unused 是一个引用了 originalThing 的闭包。
这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。 当这段代码被反复执行时,内存会持续增长。
- DOM 引用
var elements = {
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
document.body.removeChild(document.getElementById('image'));
// 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}
2
3
4
5
6
7
8
9
10
上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。
怎么避免
记住一个原则:不用的东西,及时归还
- 避免意外的全局变量
- 使用定时器和回调函数要记得回收
- 避免反复运行引发大量闭包
- 避免脱离的 DOM 元素
一般可以通过页面加载完毕后执行:performance.getEntriesByType('navigation')
即可获取所有信息
# 把 10 万次 for 循环的代码插到 html 中间,会有什么现象?出现卡顿现象怎么解决?添加 defer 属性之后脚本会在什么时候执行?采用 defer 之后,用户点击页面会怎么样?如果禁用 WebWoker,还有其他方法吗?
十万次循环代码插入 body 中,页面会出现卡顿
十万次循环代码插入 body 中,页面会出现卡顿,代码后的 DOM 节点加载不出来
解决
设置 script 标签 defer 属性,浏览器其它线程将下载脚本,待到文档解析完成脚本才会执行。
采用 defer 之后,用户点击问题
若 button 中的点击事件在 defer 脚本前定义,则在 defer 脚本加载完后,响应点击事件。
若 button 中的点击事件在 defer 脚本后定义,则用户点击 button 无反应,待脚本加载完后,再次点击有响应。
如果禁用 WebWoker,还有其他方法吗?
一、 使用 Concurrent.Thread.js
Concurrent.Thread.js (库) 用来模拟多线程,进行多线程开发
二、 使用虚拟列表
若该情形是渲染十万条数据的情况下,则可以使用虚拟列表。虚拟列表即只渲染可视区域的数据,使得在数据量庞大的情况下,减少 DOM 的渲染,使得列表流畅地无限滚动。
实现方案:
基于虚拟列表是渲染可视区域的特性,我们需要做到以下三点
需计算顶部和底部不可视区域留白的高度,撑起整个列表高度,使其高度与没有截断数据时一样,这两个高度分别命名为 topHeight、bottomHeight
计算截断开始位置 start 和结束位置 end,则可视区域的数据为 list.slice(start,end)
滚动过程中需不断更新 topHeight、bottomHeight、start、end,从而更新可视区域视图。当然我们需要对比老旧 start、end 来判断是否需要更新。
topHeight 的计算比较简单,就是滚动了多少高度,topHeight=scrollTop。
start 的计算依赖于 topHeight 和每项元素的高度 itemHeight,假设我们向上移动了两个列表项,则 start 为 2,如此,我们有 start = Math.floor(topHeight / itemHeight)。
end 的计算依赖于屏幕的高度能显示多少个列表项,我们称之为 visibleCount,则有 visibleCount = Math.ceil(clientHeight / itemHeight),向上取整是为了避免计算偏小导致屏幕没有显示足够的内容,则 end = start + visibleCount。
bottomHeight 需要我们知道整个列表没有被截断前的高度,减去其顶部的高度,计算顶部的高度有了 end 就很简单了,假设我们的整个列表项的数量为 totalItem,则 bottomHeight = (totalItem - end - 1) * itemHeight。
# 长列表优化,多种方案及对比
使用 requestAnimationFrame
与 setTimeout 相比,requestAnimationFrame 最大的优势是由系统来决定回调函数的执行时机。
如果屏幕刷新率是 60Hz,那么回调函数就每 16.7ms 被执行一次,如果刷新率是 75Hz,那么这个时间间隔就变成了 1000/75=13.3ms,换句话说就是, requestAnimationFrame 的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象。
我们使用 requestAnimationFrame 来进行分批渲染:
<ul id="container"></ul>
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
if(curTotal <= 0){
return false;
}
//每页多少条
let pageCount = Math.min(curTotal , once);
window.requestAnimationFrame(function(){
for(let i = 0; i < pageCount; i++){
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount,curIndex + pageCount)
})
}
loop(total,index);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
使用 DocumentFragment
从 MDN 的说明中,我们得知 DocumentFragments 是 DOM 节点,但并不是 DOM 树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。 当 append 元素到 document 中时,被 append 进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 可以得到样式的计算值。
而 append 元素到 documentFragment 中时,是不会计算元素的样式表,所以documentFragment 性能更优。当然现在浏览器的优化已经做的很好了, 当 append 元素到 document 中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。
<ul id="container"></ul>
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
if(curTotal <= 0){
return false;
}
//每页多少条
let pageCount = Math.min(curTotal , once);
window.requestAnimationFrame(function(){
let fragment = document.createDocumentFragment();
for(let i = 0; i < pageCount; i++){
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount,curIndex + pageCount)
})
}
loop(total,index);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
虚拟列表
用数组保存所有列表元素的位置,只渲染可视区内的列表元素,当可视区滚动时,根据滚动的 offset 大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素。
在React项目中,如何优雅的优化长列表 (opens new window)
intersectionobserver
利用 intersectionObserver 监听已渲染的 DOM 列表首末两个 item,在首末两个item 进入视口的时候渲染上一页或下一页的列表项。
为了能够保持当前所占据的位置,通过设置 padding,来替代已经渲染过的列表上方的 item,假装上方有 DOM 已经渲染。
实践:这个长列表优化渲染,值得学一波 (opens new window)
常用的库
- vue-virtual-scroller
- vue-virtual-scroll-list
- react-virtualized
- react-tiny-virtual-list
# DOM 树上有 10 个节点,渲染树上一定有 10 个节点吗?
不一定。
DOM(Document Object Model)树表示网页文档的结构,包括HTML元素、文本节点和其他类型的节点。渲染树(Render Tree)是由浏览器根据DOM树和CSS样式信息生成的,用于渲染网页内容的树结构。
虽然DOM树上有10个节点,但是在生成渲染树时,某些节点可能会被忽略或合并。例如,一些隐藏的元素、没有可见内容的元素或通过CSS样式设置为不可见的元素可能会被省略。此外,一些DOM节点可能会被合并为单个渲染树节点,以提高渲染效率。
因此,渲染树上的节点数量可能会少于DOM树的节点数量。渲染树的具体结构和节点数量取决于DOM树的内容、CSS样式和其他渲染相关的因素。
# 怎么衡量一个页面的渲染速度,性能
衡量一个页面的渲染速度和性能可以使用以下指标:
页面加载时间(Page Load Time):页面加载时间是指从用户请求页面到页面完全加载并可交互所需的时间。它可以通过浏览器的开发者工具或性能分析工具进行测量。较短的页面加载时间通常表示更好的性能。
首次内容渲染(First Contentful Paint,FCP):FCP是指页面上第一个DOM元素被渲染的时间点。它反映了页面加载过程中内容的可见性,较快的FCP时间通常表示更好的用户体验。
首次有效渲染(First Meaningful Paint,FMP):FMP是指页面上主要内容被渲染并可视的时间点。它衡量了页面加载过程中对用户有意义的内容的可见性,较快的FMP时间通常表示更好的用户体验。
总体渲染时间(Total Render Time):总体渲染时间是指页面从开始加载到渲染完毕的总时间。它包括网络请求时间、DOM解析时间、CSS渲染时间、JavaScript执行时间等。较短的总体渲染时间通常表示更好的性能。
请求响应时间(Response Time):请求响应时间是指从发送请求到接收到服务器响应的时间。较短的响应时间通常表示更快的网络连接和服务器响应。
除了这些指标,还可以考虑其他性能指标,如页面大小、资源加载时间、JavaScript执行时间、DOM操作次数等。对于移动设备,还可以关注电池消耗和网络流量等因素。