代码:https://github.com/zhaobao1830/xingnengyouhua
1、web前端本质上是一种前端GUI(图形界面系统)软件,本可以直接借鉴其他GUI系统架构设计方法。但web前端有点特别
B/S
2、浏览器的一个请求从发送到返回都经历了什么?
- 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
- 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
- TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
- 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
- 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400 或 500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
- 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
- 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到
script
标签的话,会判断是否存在async
或者defer
,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。 - 初始的 HTML 被完全加载和解析后会触发
DOMContentLoaded
事件 - CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
- 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
3、请求过程中一些潜在的性能优化点
dns是否可以通过缓存减少dns查询时间?
网络请求的过程走最近的网络环境?
相同的静态资源是否可以缓存?
能否减少请求http请求大小?
减少http请求(将多个js文件合并)
服务端渲染
具体来说,DNS 解析花时间,能不能尽量减少解析次数或者把解析前置?能——浏览器 DNS 缓存和 DNS prefetch。TCP 每次的三次握手都急死人,有没有解决方案?有——长连接、预连接、接入 SPDY 协议。如果说这两个过程的优化往往需要我们和团队的服务端工程师协作完成,前端单方面可以做的努力有限,那么 HTTP 请求呢?——在减少请求次数和减小请求体积方面,我们应该是专家!再者,服务器越远,一次请求就越慢,那部署时就把静态资源放在离我们更近的 CDN 上是不是就能更快一些?
以上提到的都是网络层面的性能优化。再往下走就是浏览器端的性能优化——这部分涉及资源加载优化、服务端渲染、浏览器缓存机制的利用、DOM 树的构建、网页排版和渲染过程、回流与重绘的考量、DOM 操作的合理规避等等——这正是前端工程师可以真正一展拳脚的地方。学习这些知识,不仅可以帮助我们从根本上提升页面性能,更能够大大加深个人对浏览器底层原理、运行机制的理解,一举两得!
4、Gzip 压缩原理
说到压缩,可不只是构建工具的专利。我们日常开发中,其实还有一个便宜又好用的压缩操作:开启 Gzip。
具体的做法非常简单,只需要你在你的 request headers 中加上这么一句:
accept-encoding:gzip
HTTP 压缩是一种内置到网页服务器和网页客户端中以改进传输速度和带宽利用率的方式。在使用 HTTP 压缩的情况下,HTTP 数据在从服务器发送前就已压缩:兼容的浏览器将在下载所需的格式前宣告支持何种方法给服务器;不支持压缩方法的浏览器将下载未经压缩的数据。最常见的压缩方案包括 Gzip 和 Deflate。... https://juejin.im 掘金 — 一个帮助开发者成长的社区
HTTP 压缩就是以缩小体积为目的,对 HTTP 内容进行重新编码的过程
Gzip 的内核就是 Deflate,目前我们压缩文件用得最多的就是 Gzip。可以说,Gzip 就是 HTTP 压缩的经典例题。
该不该用 Gzip
如果你的项目不是极端迷你的超小型文件,我都建议你试试 Gzip。
有的同学或许存在这样的疑问:压缩 Gzip,服务端要花时间;解压 Gzip,浏览器要花时间。中间节省出来的传输时间,真的那么可观吗?
答案是肯定的。如果你手上的项目是 1k、2k 的小文件,那确实有点高射炮打蚊子的意思,不值当。但更多的时候,我们处理的都是具备一定规模的项目文件。实践证明,这种情况下压缩和解压带来的时间开销相对于传输过程中节省下的时间开销来说,可以说是微不足道的。
Gzip 是万能的吗
首先要承认 Gzip 是高效的,压缩后通常能帮我们减少响应 70% 左右的大小。
但它并非万能。Gzip 并不保证针对每一个文件的压缩都会使其变小。
Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大。反之亦然。
webpack 的 Gzip 和服务端的 Gzip
一般来说,Gzip 压缩是服务器的活儿:服务器了解到我们这边有一个 Gzip 压缩的需求,它会启动自己的 CPU 去为我们完成这个任务。而压缩文件这个过程本身是需要耗费时间的,大家可以理解为我们以服务器压缩的时间开销和 CPU 开销(以及浏览器解析压缩文件的开销)为代价,省下了一些传输过程中的时间开销。
既然存在着这样的交换,那么就要求我们学会权衡。服务器的 CPU 性能不是无限的,如果存在大量的压缩需求,服务器也扛不住的。服务器一旦因此慢下来了,用户还是要等。Webpack 中 Gzip 压缩操作的存在,事实上就是为了在构建过程中去做一部分服务器的工作,为服务器分压。
因此,这两个地方的 Gzip 压缩,谁也不能替代谁。它们必须和平共处,好好合作。作为开发者,我们也应该结合业务压力的实际强度情况,去做好这其中的权衡。
4、网络请求的时候,会出现网络选择和缓存的问题,我们一般用cdn,但是cdn也有一个问题,cdn是请求静态资源用的,请求中携带的cookie是没有用的,所以我们希望在请求的过程中把cookie从http.request.header中去掉,但是很多时候cdn的域名会弄得和本身网站的域名相同,就会将我们主站的cookie通过网络去携带到我们的cdn的服务端,这个是对网络无谓的损耗,所以我们一定要注意,cdn的域名不要和主站的域名一样,这样就可以防止访问cdn的时候,还携带主站cookie这个问题
5、通过浏览器的缓存策略,我们可以对相同的资源和相同的接口从浏览器的缓存中读取数据
6、现在流行的一些框架,比如:vue、react都是在浏览器端渲染,不是直接显示html,是走框架中的代码渲染,这个渲染过程对于首屏就有很大的损耗,不利于前端的性能的,这种场景下,我们就有一个服务端渲染的方案,在服务端进行整个html的渲染,从而将整个html指出到浏览器端。所以我们可以做一些服务器端渲染优化的方案
7、
8
9、
js压缩的意义:减少网路请求的大小;浏览器并发请求的数量是有限的,压缩以后,减少请求数量
10、没有合并文件会出现的问题
11、
文件合并以后,如果页面显示,需要JS文件支持,那就要等这个合并以后的JS请求完成,首屏渲染会需要很长时间。这种情况容易出现在使用框架的项目中,比如vue、react,
项目要想正常运行,就需要加载完所有文件,解决办法是使用服务器端渲染
缓存失效问题,多个js文件合并,只要有一个js文件被修改,合并文件就会被修改,之前的缓存就失效了
真实环境中,公共库不容易修改,业务代码频繁修改,所以把公共库单独打包成一个文件,业务代码单独打包成一个文件,这样业务代码修改的时候,也不会影响公共库代码的缓存
不同页面的合并,就是单页应用中,每个页面的展示,都是请求当前页面的JS,所以我们把每个页面的js单独打包,在加载这个页面的时候,才加载对应的js。现在的技术是可以实现的,异步加载组件
12、
雪碧图,PC端用的多
缺点:在整个雪碧图加载完之前,里面的image都不能使用(可以拆分雪碧图)
如果页面中引入的图片比较小,那就把图片内容内嵌到html中,用base64的格式文件信息inline到html中,因为这是性能的消耗主要就是在请求图片中消耗的,如果有10个图,不可能去请求10次,最好的办法是把图片内容inline到html中,一起请求,这样可以减少请求数量
在做一些相对简单的页面的时候,用矢量图,用的是svg标签绘制一个矢量图,不需要经过http请求加载资源,同时也在整个dom树简析的时候就将这个svg进行渲染
11、
并发加载:html在加载页面的时候,会引入css和js,浏览器在同一个域名下的加载数量是有限的,所以实际情况下,我们会引入3到4个cdn,提高浏览器的并发请求上限
依赖关系:css要放在header中,不然会出现在渲染过程中,页面突然闪一下
引入方式:Js:第一种:直接用script引入,会出现阻塞;第二种:动态引入,加载到这个页面的时候才引入,这个主要是单页应用中
词法分析:浏览器对html简析的方式,从上到下,顺序执行
并发加载:html引入的资源是并发去请求的
并发上限:浏览器在同一个域名下并发请求是有上限的,我们应该控制某个域名下资源请求的数量,从而避免并发上限
12、
css在head中通过link引入,会阻塞页面的渲染
css不阻塞外部脚本的加载,但是会阻塞js的执行,比如js里需要动态改变css的样式
13、
直接引入的js阻塞页面的渲染,例如js会调用document.write这种方式,来改变dom结构,这时渲染会暂停
defer不会阻塞页面渲染,是在dom树加载完成后再加载这个js,而且这些js的加载也是按顺序加载的
async不会阻塞页面渲染,不是按顺序加载js的,哪个Js先返回,就加载谁。不会关心依赖关系,也不要有依赖关系,有依赖关系的脚本不要通过async引入
defer能保证dom树一定已经加载完整,只用script或者async不一定能保证
14、link和@import的区别
低版本浏览器中:
link支持并发,@import不支持并发,一个@import文件加载完后,才会加载另一个@import文件
整个页面加载完成后,才会执行@import中的代码
上面的俩条在高版本浏览器中已经没有了,@import也支持并发
@import的一个优点是:适合模块化开发,可以在css文件中引入另一个css文件(注意:这种情况下,只有前一个css文件执行完,才会执行引入的这个css文件,所以要避免多层引入)
15、懒加载
图片进入可视区域之后才会请求图片资源,之前图片的src为一个1kb的图片路径,真实的url值保持在data-url中,通过监听scroll事件,当触发的时候,将img标签上的data-url属性的值放到src中,这时src的变化就会触发相关资源的请求
懒加载多用于电商网站中
使用的原因是因为:一个电商网站中有多个图片,有时候看浏览网站的时候,不会都看,如果把没有进入可视区域的图片也加载,会造成资源的浪费。还有一个原因是:在网页中,img一般都在js上方,src请求的资源的时候,会占据大量的并发,进而影响Js的加载,影响网站的正常使用
在有依赖关系的场景可以使用预加载,比如这个页面需要这个图片,只有这个图片加载完以后,页面才会显示
使用场景:抽奖
16、css性能会让javascript变慢?
加载css的时候,会阻塞渲染;加载js的时候,也会阻塞渲染
在浏览器中,js解析和ul渲染是在单独的线程中,一个线程=》javascript解析,一个线程=》ui渲染,这俩个线程是互斥的,浏览器是单线程运行机制,css会影响页面的样式的展示,如果渲染的流出频繁的触发的话,就是频繁触发重绘与回流,会导致UI频繁渲染,阻塞javascript的线程,最终导致JS变慢
我们应该优化css写法, 从而让浏览器UI渲染的次数和难度降低,从而来加快渲染的速度,让整个性能有所提升
17、避免重绘和回流的方法
缺点:合成图层的时候,会大量消耗资源
18、
会破坏缓存机制
2、opacity替代visibility(这个需要让dom是独立的图层):https://coding.imooc.com/lesson/130.html#mid=6521
5、可以前面定义一个变量,把获取dom属性的值赋给这个变量,然后使用这个变量进行操作
19、浏览器存储--cookies
Cookie的生成方式:
1、服务器端生成,http response header中的set-cookie
2、通过document.cookie可以读写cookie,
获取的是cookie键值对组成的字符串:
写的时候,可以用下面这个方法:
Cookie的作用:
1、用于浏览器端和服务器端的交互
2、客户端自身数据的存储
Cookie有个很重要的属性:过期时间--expire
Cookie另一个个很重要的属性: httponly,设置以后,js无法读取和修改cookie
Cookie存储的限制:
1、 作为浏览器存储,大小4KB左右
2、需要设置过期时间--expire
在实际的项目中,Cookie存储数据能力被localstorage所替代
cdn请求静态资源的时候,会把Cookie带上,这样会消耗大量的流量
20、
实例:
想localStorage中存储数据 localStorage.setItem('key','value')
从localStorage中获取数据 loaclStorage.getItem('name')
21、
实例:
22、
PWA是谷歌开发的,是一个渐进式的Web,举个例子:手机在网不好的情况下如何快速显示内容,离线状态下该如何,1G下如何,2G下如何,4G下如何
检测是否是PWA,以及网站性能
可以理解为主线程外的另一个线程,现在的javascript是单线程的,所有的加载都在主线程中执行,js主线程可以把一些耗时比较长,不依赖页面或用户交互的特性放到这个
Service Worker中,到浏览器的后台去运行,这样可以减少主线程被阻塞的程度,同时丰富浏览器对后续特性的扩展
Service Worker俩个广泛的应用点:
一、Service Worker可以用来实现离线加载:
在没有网的情况下,发出请求,请求被Service Worker拦截,Service Worker可以使用它的缓存数据进行页面加载,实际上这个时候并没有把请求打到网络上,所以这时就算
没有网络,页面也可以渲染出来,后面如果有网了,就可以用请求到的资源来替换Service Worker渲染的dom
二、可以把一些需要消耗很长时间运算的,不影响页面渲染的功能放到后台进行执行,执行完以后,通过Service Worker与主页面有关的运算机制,传递给主页面,然后主页面
监听相关message的事件,就能拿到Service Worker的运算结果,从而进行页面后续逻辑的执行
Service Worker只能在https下用,本地开发中没有https,可以使用localhost,不能使用ip
23、http header端的缓存
Cache-Control
no-cache 浏览器请求会到服务器端,然后使用例如Last-Modified等方法进一步判断当前浏览器端的缓存有没有过期
no-store 没有缓存
max-age的优先级高于Expires
max-age和Expires都是强浏览器端缓存行为,意思就是这俩个属性生效的时候,不会触发向服务器端发起请求的过程,而是直接从我们的浏览器中读相应缓存的数据。这会有一个问题:当服务端有文件更新了,客户端是不知道的,这时候就需要Last-Modified/If-Modified-Since
流程是这样的:客户端向服务器端请求,服务端返回的Response Headers里面有last-modified,高数客户端这个资源修改的最后时间是什么时候;客户端再请求的时候,服务端就会
查看Request Headers里面有没有if-modified-since,如果有,就进行判断,看这个时间点之后有没有修改资源,没有的话,就返回304,让客户端请求客户端的缓存资源,有的话,就返回200,并且更新last-modified。
实际工作中,last-modified会和cache-control配合使用,如果cache-control设置了缓存,就会先使用客户端的缓存资源,过期以后,才会想服务器端发出请求
is-modified是以秒为单位的,如果是在毫秒里面修改了,它是察觉不到的
24、为了解决上面提到的last-modified的确定,可以使用下面的
原理还是一样,服务器端的http response header里面携带ETag到客户端,客户端请求的时候,会在Request Headers里面携带if-None-Match进行判断,如果match,就使用缓存,否则向客户端发起请求
从下往上,一层一层
在第二层,ETag的优先级比if-modified高
上面这个图的原理是:
最上层:当第一次请求的时候,直接向服务端发起请求,返回的状态码是200
第二层:当下一层的缓存时间失效,就进入这一层,经过前后端协商,然后进行判断,如果后端没有修改数据,就使用前端缓存,返回的状态码是304
最下层:由于设置了缓存,所以请求的时候,使用的是浏览器缓存的资源,返回的状态码是200(from cache)
备注:ETag和last-modified都是后端tomcat或nagix设置
25、
上面图的理解:用户从浏览器端发出请求,判断是否存在缓存,如果没有,就向服务端发起请求,服务端会请求响应,并把缓存策略告诉客户端,通过http response headers里的catche-control、Last-Modified、Etag来进行缓存协商,最终展示我们的资源。
如果存在缓存,就会简析这个缓存文件,检查缓存文件是否已经在本地过期,通过max-age、Expires判断,如果缓存没有过期,就直接使用缓存内容,返回相关的资源展示
如果缓存时间已经过期,就会去找Etag(优先级高),在http request headers里带上if-none-match向服务器端发起请求,到了服务器端,会进行判断,这个if-none-match的hash值和我服务器端的hash值是否一致,如果相同,返回304,浏览器会去取缓存文件。如果俩个hash值不一致,就会返回200,然后返回新的数据,并且协商新的缓存机制
如果没有Etag,就走last-modified分支,之后和上面的一致
Cache-Control:
是否可以重用,如果不可以,jiushino-store
如果可以,就判断每次请求是否重新验证,如果yes,就是no-cache,不走三层策略里面的最下层,都会走中间层,进行客户端和服务器端之间的协商,会产生304的情况,通过last-modified和Etag进行相关判断
如果不需要每次请求都重新验证,就使用max-age和s-maxage。是否允许被代理服务缓存,如果no就是Private,只浏览器层面的缓存,我们就设置为max-age;如果是Public,就是能被很多人共同使用,那它就是一个源代理服务器,或者是cdn服务器,这时候就需要设置s-maxage。最后就是设置过期时间
27、服务端优化(前提是服务端使用了node.js,这样前后端使用的语言就一样了,可以使用服务端的运算能力来进行相关的运算,从而减少前端的运算成本)
Vue渲染面临的问题是什么?
Vue的项目,在页面被渲染出来前,在客户端已经做了大量的运算,Vue的dom结构是在组件的template里面写的,这就面临一个问题,如果我整个页面要渲染出来,实际上是依赖于Vue框架加载的,如果整个网站都是用Vue写的,那执行流程就是:先下载vue.js,然后执行Vue.js代码,生成相应的html页面,最后通过Vue框架把这个HTML渲染到页面里。因此,首屏显示就依赖于整个Vue加载和执行,这样会有一个性能上的损耗
使用React和Vue是有代价的,代价就是需要把框架加载完,才能把页面渲染出来
这是就有个想法:我们能不能想jsp/php一样,在后端就渲染好,然后在前端直接展示
Vue的优点:开发体验好,组件库利于维护
怎么在vue这个层面对性能进行优化?
构建层模板编译:就是把vue代码使用webpack等工具,编译成可以run time的代码
数据无关的prerender的方式:把一些与数据无关的,所有用户都看见一样的页面,prerender掉,生成静态html,然后使用这个静态的html去访问相应的vue页面
服务端渲染(主要是为了解决首屏渲染问题):把一些经常改变的页面,在服务端渲染。在服务端执行相应的js,生成相应的html,然后在客户端直接进行渲染,把一些客户端需要执行的运算,在服务端完成