前言
面试应是日常工作积累的提升,考察的是系统,全面的知识脉络体系,所以只掌握工作常用的一些相关知识,肯定是不行的,我们需要查漏补缺,形成自己的知识体系,融会贯通。本系列将会梳理前端常见的知识体系
性能优化
主要分为浏览器web原生优化和基于react/vue框架之上的优化
web应用优化
首先web应用的性能需要这几个指标来衡量
| 监控项 | 说明 | 备注 |
|---|---|---|
| 首屏加载时长 | 浏览器显示第一屏页面所消耗的时间,以800x600像素尺寸为标准,从开始加载到浏览器页面显示高度达到600像素切此区域有内容显示时间 | 图片太多 |
| 白屏时长 | 指浏览器开始显示内容的时间,也叫首次渲染的时间 | 阻塞太多,会增加白屏时间 |
| 用户可交互时间 | 用户可以进行正常的点击、输入等操作,默认可以统计 domready 时间 | 通常这个时间点会绑定事件操作 |
| 总下载时间 | 页面所有资源都加载完成并呈现出来所花的时间 | 页面 onload 时间 |
| DNS解析时间 | 域名查询时长 | TTL 优化等 |
| TCP 连接时间 | 连接创建、数据传送、连接终止 | 三次握手、拥塞预防 |
| HTTP 请求时间 | client 发出请求到开始得到请求返回的时间 | 代理商、等待可复用的 TCP 连接释放的时间,域名解析, |
| HTTP 响应时间 | client 发出请求到得到响应的整个时间 | (network)网络响应时间 + Server 的响应时间 |
其中比较重要的是首屏加载时间和白屏时间:
首屏时间=首屏内容渲染结束时间点-开始请求时间点;
白屏时间=页面开始展示的时间点-开始请求时间点
以上时间都可以通过原生的Performance对象,使用api接口来获取,但是兼容性是个问题,如果做性能分析的SDK,需要做降级处理。
白屏时间:一般认为开始解析body的时间点就是页面开始展示的时间,所以可以通过在head标签的末尾插入script来统计时间节点作为页面开始展示时间节点
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8"/>
<script>
var start_time = +new Date; //测试时间起点,实际统计起点为 DNS 查询
</script>
<!-- 3s 后这个 js 才会返回 -->
<script src="./stop.js"></script>
<script>
var end_time = +new Date; //时间终点
console.log(end_time - start_time,'基本等价的白屏时间');
</script>
</head>
<body>
</body>
</html>
如果使用Performance接口的话为:
window.performance.timing.domloading-window.performance.timing.navigationStart来计算
首屏时间:首屏时间的统计比较复杂,影响页面加载速度的因素很多,一般认为图片加载比较耗时,所以在 DOM树 构建完成后会通过遍历首屏内的所有图片标签,并且监听所有图片标签onload事件,最终遍历图片标签的加载时间获取最大值,将这个最大值作为首屏时间
如果使用Performance接口的话为:
window.performance.timing.domInteractive的时间来衡量
其他统计指标,就不一一列举了
RAIL模型
衡量web应用性能有一个重要的RAIL模型:
Response----100ms
Animation---16.7ms
Idle---50ms
Load---1000ms
当响应时间超过100ms时,用户会感觉到卡顿;一秒绘制60帧,不会让人感到卡顿,所以一帧动画的绘制时间应当为16.7ms;因为响应时间不能超过100ms,所以闲置任务的处理时间不能超过50ms,页面loading时间控制在1000ms内,实现页面秒开
基于这个标准模型,进一步衍生出很多优化相关的概念:
1.JS采用事件循环的机制来实现异步任务,所以一个事件触发,到响应函数执行完的全部时间如果想控制在100ms内完成,需要主线程的函数执行在50ms内完成,事件回调函数也控制在50ms内完成,即可达到目标,也就是说,业务所有函数的执行事件都控制在50ms(理论值)内完成,是一个重要的衡量指标。
2.需要在16.7ms内完成一帧动画的绘制,这里涉及到像素管道的概念,动画的绘制包含: js执行->style计算->layout->paint->composition
上面所有步骤的总执行时间需要控制在16.7内,所以我们需要减少里面每个环节的时间,或者减少某个环节;比如,一般将js的执行时间控制在10ms内,通过合成图层来减少页面重绘,合理使用选择器来减少style计算。16.7ms的概念,就是时间切片的概念,react中的fiber架构就是基于此实现的
3.load时间控制在1000ms内,主要就是之前提到的首屏加载时间和白屏时间
有了衡量指标之后,我们就需要做点什么来优化性能,降低指标数值,web应用加载时通常会经历这几个阶段:重定向→拉取缓存→DNS查询→建立TCP链接→发起请求→接收响应→处理HTML元素→元素加载完成。对此,常见的优化项有:
使用DNS预解析
dns方面的优化包含:
-
减少DNS的请求次数
-
进行DNS预获取
浏览器对网站第一次的域名DNS解析查找流程依次为:浏览器缓存——系统缓存——路由器缓存——ISP DNS缓存——递归搜索
使用方法(具体可参考淘宝官网):
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.zhix.net">
<link rel="dns-prefetch" href="//api.share.zhix.net">
<link rel="dns-prefetch" href="//bdimg.share.zhix.net">
前端资源打包压缩
webpack作为前端打包的标配,所以需要对它进行一些配置来优化打包提及,包括:
- 使用UglifyJsPlugin压缩js
- 配置HtmlWebpackPlugin插件压缩html
- 使用mini-css-extract-plugin压缩css
- 提取公共资源
然后在服务器上开启Gzip传输压缩,这样文件传输的大小基本都到达极致了
图片资源优化
图片资源是web应用中必不可少的部分,前面统计的首屏时间就主要受它影响
-
不要在HTML里缩放图像,我们经常在200X200的区域放一张400X400的二倍图,觉得显示得更清晰,其实都是错误的,很影响性能
-
使用雪碧图,经典的图片优化技巧,永不过时。webpack-spritesmith这个webpack插件可以自动生成雪碧图,很方便
-
使用字体图标,小图标尽量全部使用iconfont,很节省空间
-
使用WebP格式图片,体积小,但兼容性不太好
-
使用在线工具对png/jpg的图片进行必要的压缩
使用CDN
常用的静态资源部署到cdn上,是优化的必要措施
页面渲染优化
页面渲染涉及两个重要概念:重排(元素布局发生修改)和重绘(元素的视觉表现属性发生改变)。
重排是由CPU处理的,而重绘是由GPU处理的,CPU的处理效率远不及GPU,并且重排一定会引发重绘,而重绘不一定会引发重排
重排和重绘是很耗费性能的,页面性能的关键就是尽量减少重排和重绘的发生,有时我们需要将经常重排和重绘的元素提取为单独的渲染层layout,和其他层进行隔离,来进行硬件加速。可以使用:
transform: translateZ(0);
backface-visibility: hidden;
来触发新的layout
页面渲染常见的优化项,来减少重排和重绘:
- 尽量不要用JS操作css样式,读取css属性会触发重排和重绘
- 通过切换class类名,来批量修改属性
- 使用Document Fragment对象来批量操作DOM,框架内部都是这么做的
- 将没用的元素设为不可见
- 控制DOM的深度,不要有过深的子元素
- 图片在渲染前指定大小
- 大量重排重绘的元素单独触发渲染层
负载均衡
偏向服务侧,我们可以通过配置负载均衡服务器,来加速响应时间和请求处理时间。一般前端都是搭建node中间层来实现,可以使用pm2的进程管理模块提供的整体解决方案,这里说明下,反向代理是对服务器实现负载均衡,而pm2是对进程实现负载均衡
接下来是框架层面的优化
框架通用优化项
- 避免组件层级嵌套过深,合理划分组件
- 避免state数据嵌套过深,对数据进行拍平
- 大量列表数据,使用虚拟列表来渲染,即只渲染页面视口可见部门的数据,滚动后,渲染其他部分的数据
- 组件按需加载
- 使用服务端渲染(SSR)和预渲染(Prerender)
react框架的常见优化措施
列表渲染时指定key,来快速定位需要更新的元素
使用shouldComponentUpdate来手动判断是否需要更新组件
使用pureComponent组件来自动进行props的浅比较
慎用箭头函数
class Button extends React.Component {
render() {
return <button onClick={() => {console.log('xxxx');}}>click</button>
}
}
这种内联函数,每次组件更新渲染时,都会重新生成一份
class Button extends React.Component {
handleClick = () => {
console.log('xxxx');
}
render() {
return <button onClick={handleClick}>click</button>
}
}
先声明好事件监听函数后,然后再拿到其引用传给组件
使用useCallback对一些复杂计算函数进行缓存
export const Button = (text, alertMsg) => {
const handleClick = useCallback(() => {
// do something with alertMsg
}, [alertMsg]);
return (
<button onClick={handleClick}>{text}</button>
);
}
只有依赖项alertMsg改变,handleClick才会更新
使用useMemo来缓存计算结果
使用React.Memo来缓存组件
使用redux配套的reselect库,只有依赖项改变才会重新计算state
使用Suspense配合React.lazy来实现组件的按需加载
VUE框架的常见优化项
使用Object.freeze()冻结对象
vue默认会对data中的数据进行数据劫持,对于不需要响应式的数据,可以使用Object.freeze()冻结,vue就不会加上setter getter 等数据劫持的方法
避免持久化 Store 数据
vuex-persistedstate等工具利用localstorage来将数据进行持久化,需要合理使用,以及对数据进行及时清空
合理使用v-if和v-show
合理使用computed
合理使用keep-alive
vuex-persistedstate等工具利用localstorage来将数据进行持久化,需要合理使用,以及对数据进行及时清空
浏览器缓存
性能优化和缓存是分不开的,了解浏览器的缓存策略并合理利用,可以显著提升应用性能
上图是浏览器缓存的完整流程,能用自己的理解口述清楚,基本没问题
缓存中有两个常见场景需要了解下:
- 频繁修改的资源
设置Cache-Control:no-cache,让浏览器每次获取资源都向服务器请求下,然后配合ETag或者Last-Modified来验证资源是否有效
- 不常变化的资源
设置Cache-Control: max-age=31536000,这样浏览器都会命中强缓存,如果需要更新资源,可以在资源路径后拼接hash参数来更改url来让强缓存失效
http协议
通过上面的了解,会发现浏览器中需要涉及很多http相关的知识,简单梳理下
Http协议1.0: 定义了http请求和响应的主要格式,分为请求/响应行,头部,主体三大部分。定义了常见的字段:Content-Type,cache-control,Connection等等
主要缺点/问题:每个TCP连接只能发送一个请求。无法复用TCP通道,性能较差
Http协议1.1: 主要使用的经典版本,使用持久连接,需要客户端通过Connection: close主动关闭TCP连接,浏览器允许同一域名同时建立6个持久连接(记住,可能会考)
引入管道机制,同一个TCP连接里面,客户端可以同时发送多个请求
响应头增加了Content-Length字段,标示请求的资源大小,请求头中增加Host字段
主要缺点/问题:队头阻塞(请求顺序处理)。解决办法:一是减少请求数,二是同时多开持久连接
HTTP/2: 头信息和数据体都是二进制(头信息帧和数据帧);使用多工解决队头阻塞;服务器推送(主动向客户端发送资源)
接下来,还有一个经常的考点就是http的各种状态码,考前过一遍,能记住多少就记多少吧
http的水比较深,前端掌握了这些基本知识,基本就不会减分了,如果确实对这感兴趣,可以细细研究下。
后记
梳理一下,才发现前端的基础知识都有这么多,预期3-4篇才能基本罗列完。然而后面还有框架篇,算法篇,架构篇。。。,前端确实越来越难混了,但愿大家都能坚持,不断学习补充自己