剑指Offer之基础篇

368 阅读11分钟

前言

面试应是日常工作积累的提升,考察的是系统,全面的知识脉络体系,所以只掌握工作常用的一些相关知识,肯定是不行的,我们需要查漏补缺,形成自己的知识体系,融会贯通。本系列将会梳理前端常见的知识体系

性能优化

主要分为浏览器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方面的优化包含:

  1. 减少DNS的请求次数

  2. 进行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作为前端打包的标配,所以需要对它进行一些配置来优化打包提及,包括:

  1. 使用UglifyJsPlugin压缩js
  2. 配置HtmlWebpackPlugin插件压缩html
  3. 使用mini-css-extract-plugin压缩css
  4. 提取公共资源

然后在服务器上开启Gzip传输压缩,这样文件传输的大小基本都到达极致了

图片资源优化

图片资源是web应用中必不可少的部分,前面统计的首屏时间就主要受它影响

  1. 不要在HTML里缩放图像,我们经常在200X200的区域放一张400X400的二倍图,觉得显示得更清晰,其实都是错误的,很影响性能

  2. 使用雪碧图,经典的图片优化技巧,永不过时。webpack-spritesmith这个webpack插件可以自动生成雪碧图,很方便

  3. 使用字体图标,小图标尽量全部使用iconfont,很节省空间

  4. 使用WebP格式图片,体积小,但兼容性不太好

  5. 使用在线工具对png/jpg的图片进行必要的压缩

使用CDN

常用的静态资源部署到cdn上,是优化的必要措施

页面渲染优化

页面渲染涉及两个重要概念:重排(元素布局发生修改)和重绘(元素的视觉表现属性发生改变)。

重排是由CPU处理的,而重绘是由GPU处理的,CPU的处理效率远不及GPU,并且重排一定会引发重绘,而重绘不一定会引发重排

重排和重绘是很耗费性能的,页面性能的关键就是尽量减少重排和重绘的发生,有时我们需要将经常重排和重绘的元素提取为单独的渲染层layout,和其他层进行隔离,来进行硬件加速。可以使用:

transform: translateZ(0);
backface-visibility: hidden;

来触发新的layout

页面渲染常见的优化项,来减少重排和重绘:

  1. 尽量不要用JS操作css样式,读取css属性会触发重排和重绘
  2. 通过切换class类名,来批量修改属性
  3. 使用Document Fragment对象来批量操作DOM,框架内部都是这么做的
  4. 将没用的元素设为不可见
  5. 控制DOM的深度,不要有过深的子元素
  6. 图片在渲染前指定大小
  7. 大量重排重绘的元素单独触发渲染层

负载均衡

偏向服务侧,我们可以通过配置负载均衡服务器,来加速响应时间和请求处理时间。一般前端都是搭建node中间层来实现,可以使用pm2的进程管理模块提供的整体解决方案,这里说明下,反向代理是对服务器实现负载均衡,而pm2是对进程实现负载均衡

接下来是框架层面的优化

框架通用优化项

  1. 避免组件层级嵌套过深,合理划分组件
  2. 避免state数据嵌套过深,对数据进行拍平
  3. 大量列表数据,使用虚拟列表来渲染,即只渲染页面视口可见部门的数据,滚动后,渲染其他部分的数据
  4. 组件按需加载
  5. 使用服务端渲染(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来将数据进行持久化,需要合理使用,以及对数据进行及时清空

浏览器缓存

性能优化和缓存是分不开的,了解浏览器的缓存策略并合理利用,可以显著提升应用性能

上图是浏览器缓存的完整流程,能用自己的理解口述清楚,基本没问题

缓存中有两个常见场景需要了解下:

  1. 频繁修改的资源

设置Cache-Control:no-cache,让浏览器每次获取资源都向服务器请求下,然后配合ETag或者Last-Modified来验证资源是否有效

  1. 不常变化的资源

设置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篇才能基本罗列完。然而后面还有框架篇,算法篇,架构篇。。。,前端确实越来越难混了,但愿大家都能坚持,不断学习补充自己