从输入URL到页面加载完成的整个过程
首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据(如果使用HTTPS)
数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 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 绘制,合成图层,将内容显示在屏幕上了
没有要传输的文件了,断开TCP连接 4 次挥手
性能优化分析
根据上面的过程可以看到,页面的加载过程主要分为下载、解析、渲染三个步骤,整体可以从两个角度来考虑:
网页的资源请求与加载阶段
网页渲染阶段
网页的资源请求与加载阶段
我们可以打开 Chrome 的调试工具来分析此阶段的性能指标
在这里插入图片描述
在建立 TCP 连接的阶段(HTTP 协议是建立在 TCP 协议之上的)
Queuing 和 Stalled 表示请求队列以及请求等待的时间
DNS Lookup 表示执行 DNS 查询所用的时间。页面上的每一个新域都需要完整的往返才能执行DNS查询
Initila connection 和 SSL 包括 TCP 握手重试和协商 SSL 以及 SSL 握手的时间。
在请求响应的阶段
Request sent 是发出网络请求所用的时间,通常不会超过 1ms
Watiting(TTFB) 是等待初始响应所用的时间,也称为等待返回首个字节的时间,该时间将捕捉到服务器往返的延迟时间,以及等待服务器传送响应所用的时间。
Content Download 则是从服务器上接收数据的时间。
资源请求阶段优化方案
依据上面的指标给出以下几点优化方案(仅供参考)
1、划分子域
条件:拥有多个域名
Chrome 浏览器只允许每个源拥有 6 个 TCP 连接,因此可以通过划分子域的方式,将多个资源分布在不同子域上用来减少请求队列的等待时间。然而,划分子域并不是一劳永逸的方式,多个子域意味着更多的 DNS 查询时间。通常划分为 3 到 5 个比较合适。
对如何拆分资源有如下建议:
前端类:把项目业务本身的 html、css、js、图标等归为一类
静态类:CDN 资源
动态类:后端 API
2、DNS 预解析
DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP,方法是在 head 标签里写上几个 link 标签
3、预加载
在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。
预加载其实是声明式的 fetch,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载:
4、保持持久连接
HTTP 是一个无状态的面向连接的协议,即每个 HTTP 请求都是独立的。然而无状态并不代表 HTTP 不能保持 TCP 连接,Keep-Alive 正是 HTTP 协议中保持 TCP 连接非常重要的一个属性。 HTTP1.1 协议中,Keep-Alive 默认打开,使得通信双方在完成一次通信后仍然保持一定时长的连接,因此浏览器可以在一个单独的连接上进行多个请求,有效地降低建立 TCP 请求所消耗的时间。
5、CND 加速
使用 CND 加速可以减少客户端到服务器的网络距离。
CDN 的意图就是尽可能地减少资源在转发、传输、链路抖动等情况下顺利保障信息的连贯性;
CDN 系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上
CDN 采用各节点缓存的机制,当我们项目的静态资源修改后,如果 CDN 缓存没有做相应更新,则看到的还是旧的网页,解决的办法是刷新缓存,七牛云、腾讯云都可单独针对某个文件/目录进行刷新;
CDN 缓存需要合理地使用:图片、常用 js 组件、css 重置样式等,即不常改动的文件可走 CDN,包括项目内的一些介绍页;
还有一种比较流行的做法是让一些项目依赖走 CDN,比如 vuex、vue-router 这些插件通过外链的形式来引入,因为它们都有自己免费的 CDN,这样可以减少打包后的文件体积。
6、设置缓存
缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度。
通常浏览器缓存策略分为两种:强缓存 和 协商缓存。
强缓存:实现强缓存可以通过两种响应头实现:Expires和Cache-Control强缓存表示在缓存期间不需要向服务器发送请求
协商缓存:缓存过期了就是用协商缓存,其通过Last-Modified/If-Modified-Since和ETag/If-None-Match实现
HTTP 头中与缓存相关的属性,主要有以下几个:
(1) Expires: 指定缓存过期的时间,是一个绝对时间,但受客户端和服务端时钟和时区差异的影响,是 HTTP/1.0 的产物
形如Expires: Wed, 22 Oct 2018 08:41:00 GMT
(2) Cache-Control:比 Expires 策略更详细,max-age 优先级比 Expires 高,其值可以是以下五种情况
no-cache: 强制所有缓存了该响应的缓存用户,在使用已存储的缓存数据前,发送请求到原始服务器(进行过期认证),通常情况下,过期认证需要配合 etag 和 Last-Modified 进行一个比较
no-store: 告诉客户端不要响应缓存(禁止使用缓存,每一次都重新请求数据)
public: 缓存响应,并可以在多用户间共享(与中间代理服务器相关)
private: 缓存响应,但不能在多用户间共享(与中间代理服务器相关)
max-age: 缓存在指定时间(单位为秒)后过期
(3) Last-Modified / If-Modified-Since: Last-Modified表示本地文件最后修改日期,If-Modified-Since会将上次从服务器获取的Last-Modified的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。
但是如果(服务器)在本地打开缓存文件(或者删了个字符 a 后又填上去),就会造成Last-Modified被修改,所以在 HTTP / 1.1 出现了ETag。
(4) Etag / If-None-Match: ETag类似于文件指纹,If-None-Match会将当前ETag发送给服务器,询问该资源ETag是否变动,有变动的话就将新的资源发送回来。并且ETag优先级比Last-Modified高。
由于 etag 要使用少数的字符表示一个不定大小的文件(如 etag: "58c4e2a1-f7"),所以 etag 是有重合的风险的,如果网站的信息特别重要,连很小的概率如百万分之一都不允许,那么就不要使用 etag 了。使用 etag 的代价是增加了服务器的计算负担,特别是当文件比较大时。
在这里插入图片描述
选择合适的缓存策略
对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略
对于某些不需要缓存的资源,可以使用Cache-control: no-store,表示该资源不需要缓存
对于频繁变动的资源,可以使用Cache-Control: no-cache并配合ETag使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。
对于代码文件来说,通常使用Cache-Control: max-age=31536000并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件。
7、使用 HTTP / 2.0
因为浏览器会有并发请求限制,在 HTTP / 1.1 时代,每个请求都需要建立和断开,消耗了好几个 RTT 时间,并且由于 TCP 慢启动的原因,加载体积大的文件会需要更多的时间。
在 HTTP / 2.0 中引入了多路复用,能够让多个请求使用同一个 TCP 链接,极大的加快了网页的加载速度。并且还支持 Header 压缩,进一步的减少了请求的数据大小。
8、图片和文件压缩
这又涉及到很多知识点了,简单来说,我们要尽可能地在保证我们的 App 能正常运行、图片尽可能保证高质量的前提下去压缩所有用到的文件的体积。比如图片格式的选择、去掉我们代码中的注释、空行、无关代码等。
图片相关优化
不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
小图使用 base64 格式
选择正确的图片格式:
对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
照片使用 JPEG
构建工具的使用
对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩
使用 ES6 模块来开启 tree shaking,这个技术可以移除没有使用的代码
优化图片,对于小图可以使用 base64 的方式写入文件中
按照路由拆分代码,实现按需加载
给打包出来的文件名添加哈希,实现浏览器缓存文件(能及时更新)
启用 gzip 压缩(需要前后端支持)
各种 loader/plugin 的使用
压缩 HTML 文件
可以把 HTML 的注释去掉,把行前缩进删掉,这样处理的文件可以明显减少 HTML 的体积;这样做几乎是没有风险的,除了 pre 标签不能够去掉行首缩进之外,其他的都正常。
网页渲染阶段优化方案
1、
在 webpack 里有个 externals 选项,可以忽略不需要打包的库
https://webpack.js.org/configuration/externals/#root
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios'
},
output: {
...
}
}
2、路由懒加载
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/ebook',
component: () => import('./views/ebook/index.vue'), // 路由懒加载,这里用的是ES6的语法 import()函数是动态加载 import 是静态加载
children: [ // 动态路由, 可以传递路径参数
{
path: ':fileName',
component: () => import('./components/ebook/EbookReader.vue')
}
]
},
{
path: '/store',
component: () => import('./views/store/index.vue'),
redirect: '/store/shelf', // #/store -> #/store/home
...
}
]
})
3、使用懒加载插件 Vue-Loader
具体的使用可以参考 这篇文章 或者去看官方文档
step1:cnpm install vue-lazyload --save
step2:main.js导入
import VueLazyLoad from 'vue-lazyload'
Vue.use(VueLazyload)
step3:vue 文件中将需要懒加载的图片绑定v-bind:src修改为v-lazy
这只是图片懒加载,还有很多其他可选配置
4、v-if与v-show的选择
一般来说,v-if有更高的切换开销,而v-show有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show较好;如果在运行时条件很少改变,则使用v-if较好。
React 项目优化点
1、单个组件的优化:更改 shouldComponentUpdate 函数的默认实现,根据每个 React 组件的内在逻辑定制其行为,减少不必要的重新渲染
shouldComponentUpdate(nextProps, nextState) {
// 假设影响渲染内容的 prop 只有 completed 和 text,只需要确保
// 这两个 prop 没有变化,函数就可以返回 false
return (nextProps.completed !== this.props.completed) ||
(nextProps.text !== this.props.text)
}
2、使用 immutable.js 解决复杂数据 diff、clone 等问题。
immutable.js 实现原理:持久化数据结构,也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
3、在 constructor() 里做 this 绑定
当在 render() 里使用事件处理方法时,提前在构造函数里把 this 绑定上去(如果需要的话),因为在每次 render 过程中, 再调用 bind 都会新建一个新的函数,浪费资源.
// bad
class App extends React.Component {
onClickDiv() {
// do stuff
}
render() {
return
}
}
// good
class App extends React.Component {
constructor(props) {
super(props);
this.onClickDiv = this.onClickDiv.bind(this);
}
onClickDiv() {
// do stuff
}
render() {
return
}
}
4、基于路由的代码分割
使用React.lazy和React Router来配置基于路由的代码分割
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Suspense fallback={
);