1

前言

最近遇到了关于优化的问题,查阅了很多资料,也看了很多大佬的博客,现在将我学到的内容总结到一起。


若要对前端性能进行优化,就要先了解从输入URL到页面加载完成整个工作流程,我们才能针对各部分进行优化。

借助大佬一张图:
借助大佬一张图

整个流程是这样的:

  1. DNS域名解析
  2. TCP三次握手建立连接
  3. 客户端发送HTTP请求到服务器
  4. 服务端处理请求,HTTP响应返回
  5. 浏览器拿到相应数据,解析响应内容,页面加载内容
  6. 渲染

那么我们若想优化,可以从这几方面入手。
整个优化过程
image
若要完成这一系列,网络必不可少,所有在这之前我们先对网络进行优化。

一、优化网络连接

1、CDN加速

CDN(content distribute network,内容分发网络)的本质仍然是一个缓存,而且将数据缓存在离用户最近的地方,使用户以最快速度获取数据,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。即所谓网络访问第一跳,反向代理,如下图:
1
传统代理服务器位于浏览器一侧,代理浏览器将http请求发送到互联网上,而反向代理服务器位于网站机房一侧,代理网站web服务器接收http请求。
image
网站安全的作用,来自互联网的访问请求必须经过代理服务器,相当于web服务器和可能的网络攻击之间建立了一个屏障。

除了安全功能代理服务器也可以通过配置缓存功能加速web请求。当用户第一次访问静态内容的时候,静态内容就被缓存在反向代理服务器上,这样当其他用户访问该静态内容的时候,就可以直接从反向代理服务器返回,加速web请求响应速度,减轻web服务器负载压力。事实上,有些网站会把动态内容也缓存在代理服务器上,比如维基百科及某些博客论坛网站,把热门词条、帖子、博客缓存在反向代理服务器上加速用户访问速度,当这些动态内容有变化时,通过内部通知机制通知反向代理缓存失效,反向代理会重新加载最新的动态内容再次缓存起来。

此外,反向代理也可以实现负载均衡的功能,而通过负载均衡构建的应用集群可以提高系统总体处理能力,进而改善网站高并发情况下的性能。

2、并行连接

由于在HTTP1.1协议下,chrome每个域名的最大并发数是6个。使用多个域名,可以增加并发数

3、持久连接

使用keep-alive或presistent来建立持久连接,持久连接降低了时延和连接建立的开销,将连接保持在已调谐状态,而且减少了打开连接的潜在数量

4、管道化连接

在HTTP2协议中,可以开启管道化连接,即单条连接的多路复用(tcp),每条连接中并发传输多个资源,这里就不需要添加域名来增加并发数了

二、DNS解析优化---DNS预解析

DNS解析过程 作为了解。

DNS 实现域名到IP的映射。通过域名访问站点,每次请求都要做DNS解析。目前每次DNS解析,通常在200ms以下。一般采用DNS Prefetch 一种DNS 预解析技术,当你浏览网页时,浏览器会在加载网页时对网页中的域名进行解析缓存,这样在你单击当前网页中的连接时就无需进行DNS的解析,减少用户等待时间,提高用户体验。

<link rel="dns-prefetch" href="www.baidu.com" />  
只有部分浏览器支持

对以上几个网站提前解析 DNS,由于它是并行的,不会堵塞页面渲染,这样可以缩短资源加载的时间

三、请求过程优化

1、HTTP请求优化

(1)减少请求次数

1> 图片优化
2> 合并
合并CSS、合并javascript、合并图片。将浏览器一次访问需要的javascript和CSS合并成一个文件,这样浏览器就只需要一次请求。图片也可以合并,多张图片合并成一张,如果每张图片都有不同的超链接,可通过CSS偏移响应鼠标点击操作,构造不同的URL。
如果不进行文件合并,有如下3个隐患
  a、文件与文件之间有插入的上行请求,增加了N-1个网络延迟
  b、受丢包问题影响更严重
  c、经过代理服务器时可能会被断开
  但是,文件合并本身也有自己的问题
  a、首屏渲染问题
  b、缓存失效问题
  所以,对于文件合并,有如下改进建议
  a、公共库合并
  b、不同页面单独合并
3> 减少重定向
 尽量避免使用重定向,当页面发生了重定向,就会延迟整个HTML文档的传输。在HTML文档到达之前,页面中不会呈现任何东西,也没有任何组件会被下载,降低了用户体验

  如果一定要使用重定向,如http重定向到https,要使用301永久重定向,而不是302临时重定向。因为,如果使用302,则每一次访问http,都会被重定向到https的页面。而永久重定向,在第一次从http重定向到https之后 ,每次访问http,会直接返回https的页面
4> 使用HTTP缓存

image
此图为浏览器请求过程

使用cach-control或expires这类强缓存时,缓存不过期的情况下,不向服务器发送请求。强缓存过期时,会使用last-modified或etag这类协商缓存,向服务器发送请求,如果资源没有变化,则服务器返回304响应,浏览器继续从本地缓存加载资源;如果资源更新了,则服务器将更新后的资源发送到浏览器,并返回200响应

HTTP缓存
HTTP缓存与浏览器缓存区别
image
5> 不使用CSS @import,CSS的@import会造成额外的请求
6> 避免使用空的src和href
a标签设置空的href,会重定向到当前的页面地址
  form设置空的method,会提交表单到当前的页面地址

(2)http压缩-减少资源大小

1> 采用Gzip压缩:HTTP 压缩就是以缩小体积为目的,对 HTTP 内容进行重新编码的过程,原理是找出一些重复出现的字符串、临时替换它们,从而使整个文件变小,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大
2> webp:在安卓下可以使用webp格式的图片,它具有更优的图像数据压缩算法,能带来更小的图片体积,同等画面质量下,体积比jpg、png少了25%以上,而且同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性
3> 代码压缩:
a、HTML压缩
  HTML代码压缩就是压缩在文本文件中有意义,但是在HTML中不显示的字符,包括空格,制表符,换行符等
b、CSS压缩
  CSS压缩包括无效代码删除与CSS语义合并
c、JS压缩与混乱
  JS压缩与混乱包括无效字符及注释的删除、代码语义的缩减和优化、降低代码可读性,实现代码保护
d、图片压缩
  针对真实图片情况,舍弃一些相对无关紧要的色彩信息

(3)构建工具性能优化---webpack

1> 打包公共代码
  使用CommonsChunkPlugin插件,将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存到缓存中供后续使用。这会带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件
  webpack 4 将移除 CommonsChunkPlugin, 取而代之的是两个新的配置项 optimization.splitChunks 和 optimization.runtimeChunk
  通过设置 optimization.splitChunks.chunks: "all" 来启动默认的代码分割配置项
2> 动态导入和按需加载
  webpack提供了两种技术通过模块的内联函数调用来分离代码,优先选择的方式是,使用符合 ECMAScript 提案 的 import() 语法。第二种,则是使用 webpack 特定的 require.ensure
3> 剔除无用代码
  tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup
  JS的tree shaking主要通过uglifyjs插件来完成,CSS的tree shaking主要通过purify CSS来实现的
4> 长缓存优化
  1、将hash替换为chunkhash,这样当chunk不变时,缓存依然有效
  2、使用Name而不是id
  每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变
  下面来使用两个插件解决这个问题。第一个插件是 NamedModulesPlugin,将使用模块的路径,而不是数字标识符。虽然此插件有助于在开发过程中输出结果的可读性,然而执行时间会长一些。第二个选择是使用 HashedModuleIdsPlugin,推荐用于生产环境构建
5> 公用代码内联
  使用html-webpack-inline-chunk-plugin插件将mainfest.js内联到html文件中

2、减少网络请求--本地存储

浏览器缓存

对一个网站而言,CSS、javascript、logo、图标这些静态资源文件更新的频率都比较低,而这些文件又几乎是每次http请求都需要的,如果将这些文件缓存在浏览器中,可以极好的改善性能。通过设置http头中的cache-control和expires的属性,可设定浏览器缓存,缓存时间可以是数天,甚至是几个月。
在某些时候,静态资源文件变化需要及时应用到客户端浏览器,这种情况,可通过改变文件名实现,即更新javascript文件并不是更新javascript文件内容,而是生成一个新的JS文件并更新HTML文件中的引用。
使用浏览器缓存策略的网站在更新静态资源时,应采用逐量更新的方法,比如需要更新10个图标文件,不宜把10个文件一次全部更新,而是应该一个文件一个文件逐步更新,并有一定的间隔时间,以免用户浏览器忽然大量缓存失效,集中更新缓存,造成服务器负载骤增、网络堵塞的情况。

四、渲染优化

浏览器渲染机制

  • DOM树:
    解析 HTML 以创建的是 DOM 树(DOM tree ):渲染引擎开始解析 HTML 文档,转换树中的标签到 DOM 节点,它被称为“内容树”。
  • CSSOM树:
    解析 CSS(包括外部 CSS 文件和样式元素)创建的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的。
    -渲染树:
    CSSOM 与 DOM 结合,之后我们得到的就是渲染树(Render tree )。
  • 布局渲染树:
    从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,我们便得到了基于渲染树的布局渲染树(Layout of the render tree)。
  • 绘制渲染树:
    遍历渲染树,每个节点将使用 UI 后端层来绘制。整个过程叫做绘制渲染树(Painting the render tree)。

1、服务器ssr渲染

  • 定义:
    服务端渲染的模式下,当⽤户第⼀次请求⻚⾯时,由服务器把需要的组件或⻚⾯渲染成 HTML字符串,然后把它返回给客户端。客户端拿到⼿的,是可以直接渲染然后呈现给⽤户的 HTML 内容,不需要为了⽣成 DOM 内容⾃⼰再去跑⼀遍 JS 代码。所见即为所得
  • 优缺点:
    SEO :可以有“现成的内容”拿给搜索引擎看
    ⾸屏加载速度:服务端渲染模式下,服务器给到客户端的已经是⼀个服务端处理好的可以拿来呈现给⽤户的⽹⻚
  • 缺点: ⾮常吃硬件资源

2、优化资源加载

1> 资源加载位置

  通过优化资源加载位置,更改资源加载时机,使尽可能快地展示出页面内容,尽可能快地使功能可用
  a、CSS文件放在head中,先外链,后本页
  b、JS文件放在body底部,先外链,后本页
  c、处理页面、处理页面布局的JS文件放在head中,如babel-polyfill.js文件、flexible.js文件
  d、body中间尽量不写style标签和script标签
2> 资源加载时机
  1、异步script标签
  defer:  异步加载,在HTML解析完成后执行。defer的实际效果与将代码放在body底部类似
  async: 异步加载,加载完成后立即执行
  2、模块按需加载
  在SPA等业务逻辑比较复杂的系统中,需要根据路由来加载当前页面需要的业务模块
  按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载
  webpack 提供了两个类似的技术,优先选择的方式是使用符合 ECMAScript 提案 的 import() 语法。第二种则是使用 webpack 特定的 require.ensure
  3、使用资源预加载preload和资源预读取prefetch
  preload让浏览器提前加载指定资源,需要执行时再执行,可以加速本页面的加载速度
  prefetch告诉浏览器加载下一页面可能会用到的资源,可以加速下一个页面的加载速度
  4、资源懒加载与资源预加载
  资源延迟加载也称为懒加载,延迟加载资源或符合某些条件时才加载某些资源
  资源预加载是提前加载用户所需的资源,保证良好的用户体验
  资源懒加载和资源预加载都是一种错峰操作,在浏览器忙碌的时候不做操作,浏览器空间时,再加载资源,优化了网络性能

3、DOM优化

减少重绘回流

1> 样式设置
  1、避免使用层级较深的选择器,或其他一些复杂的选择器,以提高CSS渲染效率
  2、避免使用CSS表达式,CSS表达式是动态设置CSS属性的强大但危险方法,它的问题就在于计算频率很快。不仅仅是在页面显示和缩放时,就是在页面滚动、乃至移动鼠标时都会要重新计算一次
  3、元素适当地定义高度或最小高度,否则元素的动态内容载入时,会出现页面元素的晃动或位置,造成回流
  4、给图片设置尺寸。如果图片不设置尺寸,首次载入时,占据空间会从0到完全出现,上下左右都可能位移,发生回流
  5、不要使用table布局,因为一个小改动可能会造成整个table重新布局。而且table渲染通常要3倍于同等元素时间
  6、能够使用CSS实现的效果,尽量使用CSS而不使用JS实现
2> 渲染层
  1、此外,将需要多次重绘的元素独立为render layer渲染层,如设置absolute,可以减少重绘范围
  2、对于一些进行动画的元素,使用硬件渲染,从而避免重绘和回流
3> DOM优化
  1、缓存DOM
const div = document.getElementById('div')
  由于查询DOM比较耗时,在同一个节点无需多次查询的情况下,可以缓存DOM
  2、减少DOM深度及DOM数量
  HTML 中标签元素越多,标签的层级越深,浏览器解析DOM并绘制到浏览器中所花的时间就越长,所以应尽可能保持 DOM 元素简洁和层级较少。
  3、批量操作DOM
  由于DOM操作比较耗时,且可能会造成回流,因此要避免频繁操作DOM,可以批量操作DOM,先用字符串拼接完毕,再用innerHTML更新DOM
  4、批量操作CSS样式
  通过切换class或者使用元素的style.csstext属性去批量操作元素样式
  5、在内存中操作DOM
  使用DocumentFragment对象,让DOM操作发生在内存中,而不是页面上
  6、DOM元素离线更新
  对DOM进行相关操作时,例、appendChild等都可以使用Document Fragment对象进行离线操作,带元素“组装”完成后再一次插入页面,或者使用display:none 对元素隐藏,在元素“消失”后进行相关操作
  7、DOM读写分离
  浏览器具有惰性渲染机制,连接多次修改DOM可能只触发浏览器的一次渲染。而如果修改DOM后,立即读取DOM。为了保证读取到正确的DOM值,会触发浏览器的一次渲染。因此,修改DOM的操作要与访问DOM分开进行
  8、事件代理
  事件代理是指将事件监听器注册在父级元素上,由于子元素的事件会通过事件冒泡的方式向上传播到父节点,因此,可以由父节点的监听函数统一处理多个子元素的事件
  利用事件代理,可以减少内存使用,提高性能及降低代码复杂度
  9、防抖和节流
  使用函数节流(throttle)或函数去抖(debounce),限制某一个方法的频繁触发
  10、及时清理环境
  及时消除对象引用,清除定时器,清除事件监听器,创建最小作用域变量,可以及时回收内存

代码优化

1> 性能更好的API
1、用对选择器

  选择器的性能排序如下所示,尽量选择性能更好的选择器

id选择器(#myid)
类选择器(.myclassname)
标签选择器(div,h1,p)
相邻选择器(h1+p)
子选择器(ul \> li)
后代选择器(li a)
通配符选择器(\*)
属性选择器(a\[rel\="external"\])
伪类选择器(a:hover,li:nth\-child)

2、使用requestAnimationFrame来替代setTimeout和setInterval
  希望在每一帧刚开始的时候对页面进行更改,目前只有使用 requestAnimationFrame 能够保证这一点。使用 setTimeout 或者 setInterval 来触发更新页面的函数,该函数可能在一帧的中间或者结束的时间点上调用,进而导致该帧后面需要进行的事情没有完成,引发丢帧
3、使用IntersectionObserver来实现图片可视区域的懒加载
  传统的做法中,需要使用scroll事件,并调用getBoundingClientRect方法,来实现可视区域的判断,即使使用了函数节流,也会造成页面回流。使用IntersectionObserver,则没有上述问题
4、使用web worker
  客户端javascript一个基本的特性是单线程:比如,浏览器无法同时运行两个事件处理程序,它也无法在一个事件处理程序运行的时候触发一个计时器。Web Worker是HTML5提供的一个javascript多线程解决方案,可以将一些大计算量的代码交由web Worker运行,从而避免阻塞用户界面,在执行复杂计算和数据处理时,这个API非常有用
  但是,使用一些新的API的同时,也要注意其浏览器兼容性
2> 慎用 with
with(obj){ p = 1}; 代码块的行为实际上是修改了代码块中的 执行环境 ,将obj放在了其作用域链的最前端,在 with代码块中访问非局部变量是都是先从 obj上开始查找,如果没有再依次按作用域链向上查找,因此使用 with相当于增加了作用域链长度。而每次查找作用域链都是要消耗时间的,过长的作用域链会导致查找性能下降。
  因此,除非你能肯定在 with代码中只访问 obj中的属性,否则慎用 with,替代的可以使用局部变量缓存需要访问的属性。
3> 避免使用 eval和 Function
  每次 eval 或 Function 构造函数作用于字符串表示的源代码时,脚本引擎都需要将源代码转换成可执行代码。这是很消耗资源的操作 —— 通常比简单的函数调用慢 100倍以上。
  eval 函数效率特别低,由于事先无法知晓传给 eval 的字符串中的内容,eval在其上下文中解释要处理的代码,也就是说编译器无法优化上下文,因此只能有浏览器在运行时解释代码。这对性能影响很大。
  Function 构造函数比 eval略好,因为使用此代码不会影响周围代码 ;但其速度仍很慢。
  此外,使用 eval和 Function也不利于Javascript 压缩工具执行压缩。
3> 减少作用域链查找
  前文谈到了作用域链查找问题,这一点在循环中是尤其需要注意的问题。如果在循环中需要访问非本作用域下的变量时请在遍历之前用局部变量缓存该变量,并在遍历结束后再重写那个变量,这一点对全局变量尤其重要,因为全局变量处于作用域链的最顶端,访问时的查找次数是最多的。
4> 数据访问
  Javascript中的数据访问包括直接量 (字符串、正则表达式 )、变量、对象属性以及数组,其中对直接量和局部变量的访问是最快的,对对象属性以及数组的访问需要更大的开销。当出现以下情况时,建议将数据放入局部变量: 
  a. 对任何对象属性的访问超过 1次
  b. 对任何数组成员的访问次数超过 1次
  另外,还应当尽可能的减少对对象以及数组深度查找。
5> 字符串拼接
  在 Javascript中使用”+” 号来拼接字符串效率是比较低的,因为每次运行都会开辟新的内存并生成新的字符串变量,然后将拼接结果赋值给新变量。与之相比更为高效的做法是使用数组的 join方法,即将需要拼接的字符串放在数组中最后调用其 join方法得到结果。不过由于使用数组也有一定的开销,因此当需要拼接的字符串较多的时候可以考虑用此方法。

4、懒加载技术

这条策略实际上并不一定能减少 HTTP请求数,但是却能在某些条件下或者页面刚加载时减少 HTTP请求数。对于图片而言,在页面刚加载的时候可以只加载第一屏,当用户继续往后滚屏的时候才加载后续的图片。这样一来,假如用户只对第一屏的内容感兴趣时,那剩余的图片请求就都节省了。

参考博客

web前端性能&SEO优化
前端性能优化总结
前端性能优化(一)
前端性能优化的七大手段


LUYrty樱花
29 声望3 粉丝