script
标签的话,会判断是否存在async
或者defer
,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。DOMContentLoaded
事件根据上面的过程可以看到,页面的加载过程主要分为下载、解析、渲染三个步骤,整体可以从两个角度来考虑:
我们可以打开 Chrome 的调试工具来分析此阶段的性能指标
在建立 TCP 连接的阶段(HTTP 协议是建立在 TCP 协议之上的)
在请求响应的阶段
依据上面的指标给出以下几点优化方案(仅供参考)
条件:拥有多个域名
Chrome 浏览器只允许每个源拥有 6 个 TCP 连接,因此可以通过划分子域的方式,将多个资源分布在不同子域上用来减少请求队列的等待时间。然而,划分子域并不是一劳永逸的方式,多个子域意味着更多的 DNS 查询时间。通常划分为 3 到 5 个比较合适。
对如何拆分资源有如下建议:
DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP,方法是在 head 标签里写上几个 link 标签
<link rel="dns-prefetch" href="https://www.google.com">
<link rel="dns-prefetch" href="https://www.google-analytics.com">
对以上几个网站提前解析,这个过程是并行的,不会阻塞页面渲染。
在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。
预加载其实是声明式的 fetch,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载:
<link rel="preload" href="http://example.com">
预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。
HTTP 是一个无状态的面向连接的协议,即每个 HTTP 请求都是独立的。然而无状态并不代表 HTTP 不能保持 TCP 连接,Keep-Alive 正是 HTTP 协议中保持 TCP 连接非常重要的一个属性。 HTTP1.1 协议中,Keep-Alive 默认打开,使得通信双方在完成一次通信后仍然保持一定时长的连接,因此浏览器可以在一个单独的连接上进行多个请求,有效地降低建立 TCP 请求所消耗的时间。
使用 CND 加速可以减少客户端到服务器的网络距离。
还有一种比较流行的做法是让一些项目依赖走 CDN,比如 vuex、vue-router 这些插件通过外链的形式来引入,因为它们都有自己免费的 CDN,这样可以减少打包后的文件体积。
缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度。
通常浏览器缓存策略分为两种:强缓存 和 协商缓存。
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 高,其值可以是以下五种情况
(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
并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件。因为浏览器会有并发请求限制,在 HTTP / 1.1 时代,每个请求都需要建立和断开,消耗了好几个 RTT 时间,并且由于 TCP 慢启动的原因,加载体积大的文件会需要更多的时间。
在 HTTP / 2.0 中引入了多路复用,能够让多个请求使用同一个 TCP 链接,极大的加快了网页的加载速度。并且还支持 Header 压缩,进一步的减少了请求的数据大小。
这又涉及到很多知识点了,简单来说,我们要尽可能地在保证我们的 App 能正常运行、图片尽可能保证高质量的前提下去压缩所有用到的文件的体积。比如图片格式的选择、去掉我们代码中的注释、空行、无关代码等。
图片相关优化
构建工具的使用
压缩 HTML 文件
可以把 HTML 的注释去掉,把行前缩进删掉,这样处理的文件可以明显减少 HTML 的体积;这样做几乎是没有风险的,除了 pre 标签不能够去掉行首缩进之外,其他的都正常。
标签位置渲染线程和 JS 引擎线程是互斥的,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,因此建议将 script 标签放在 body 标签底部的原因。或者使用给 script 标签添加 defer 或者 async 属性 。
执行 JS 代码过长会卡住渲染,对于需要很多时间计算的代码可以考虑使用 Webworker。Webworker 可以让我们另开一个线程执行脚本(这并没有改变 JS 单线程的本质,因为新开的线程受控于主线程且不得操作 DOM)而不影响渲染。
懒加载就是将不关键的资源延后加载。
懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的src
属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为src
属性,这样图片就会去下载资源,实现了图片懒加载。
懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等。
使用场景比如抽奖动画展示过程中预先加载其他内容,或者电子书阅读章节的预加载可以使切换下一章节时更为流畅。
执行 JavaScript 的解析和 UI 渲染的两个浏览器线程是互斥的,UI 渲染时 JS 代码解析终止,反之亦然。
当 页面布局和几何属性 改变时,就会触发 回流。
当 需要更新的只是元素的某些外观 时,就会触发 重绘。
使用 CSS 预处理器时注意不要有过多的嵌套,嵌套层次过深会影响浏览器查找选择器的速度,且一定程度上会产生出很多冗余的字节。
减少 DOM 元素数量,合理利用 :after、:before 等伪类,避免页面过深的层级嵌套;
优化 JavaScript 性能,减少 DOM 操作次数(或集中操作),能有效规避页面重绘/重排;
只能说尽可能去做优化,如数据分页、首屏直出、按需加载等
为触发频率较高的函数使用函数节流
SPA:单页面富应用
动态地重写页面的部分与用户交互而不是加载新的页面。
优点:① 前后端分离 ② 页面之间切换快 ③ 后端只需提供 API
缺点:① 首屏速度慢,因为用户首次加载 SPA 框架及应用程序的代码然后才渲染页面 ② 不利于 SEO
SEO(Search Engine Optimization):搜索引擎优化
常用技术:利用
<html>
<head>
<title>标题内容title>
<meta name="description" content="描述内容">
<meta name="keyword" content="关键字1,关键字2,—">
head>
html>
SPA 应用中,通常通过 AJAX 获取数据,而这里就难以保证我们的页面能被搜索引擎正常收录到。并且有一些搜索引擎不支持执行 JS 和通过 AJAX 获取数据,那就更不用提 SEO 了。
对于有些网站而言,SEO 显得至关重要,例如主要以内容输出为主的 Quora、stackoverflow、知乎和豆瓣等等,那如何才能正常使用 SPA 而又不影响 SEO 呢 ?所以有了 SSR
SSR(Server-Side Rendering):服务端渲染
以下内容部分参考《深入浅出 React 与 Redux》- 程墨
为了量化网页性能,我们定义两个指标:
在一个 完全靠浏览器端渲染 的应用中,当用户在浏览器中打开一个页面的时候,最坏情况下没有任何缓存,需要等待三个 HTTP 请求才能到达 TTFP 的时间点:
而对于服务器端渲染,因为获取 HTTP 请求之后就会返回所有有内容的 HTML,所以在一个 HTTP 的周期之后就会提供给浏览器有意义的内容,所以首次渲染时间 TTFP 会优于完全依赖于浏览器端渲染的页面。
除了更短的 TTFP,服务器端渲染还有一个好处就是利于搜索引擎优化,虽然某些搜索引擎已经能够索引浏览器端渲染的网页,但是毕竟不是所有搜索引擎都能做到这一点,让搜索引擎能够索引到应用页面的最直接方法就是提供完整 HTML
上面的性能对比只是理论上的分析,实际上,采用服务器端渲染是否能获得更好的 TTFP 有多方面因素。
1、服务器端产生的 HTML 过大是否会影响性能?
因为服务器端渲染返回的是完整的 HTML,那么下载这个 HTML 的时间也会增长。
2、服务器端渲染的运算消耗是否是服务器能够承担得起的?
浏览器端渲染的方案下,服务器只提供静态资源,压力被分摊到了访问用户的浏览器中;如果使用服务器端渲染,每个页面请求都要产生 HTML 页面,这样服务器的压力就会很大。
React 并不是给服务器端渲染设计的,如果应用对 TTFP 要求不高,也不希望对 React 页面进行搜索引擎优化,那么没有必要使用“同构”来增加开发难度;如果希望应用的性能能更进一步,而且服务器运算资源充足,那么可以尝试。对 Vue 而言应该也是同样的道理。
最后我们来总结下服务端渲染理论上的优缺点:
优点:
缺点:
1、第三方库走 cdn
例如:
<script src="//cdn.bootcss.com/vue/2.2.5/vue.min.js">script>
<script src="//cdn.bootcss.com/vue-router/2.3.0/vue-router.min.js">script>
<script src="//cdn.bootcss.com/vuex/2.2.1/vuex.min.js">script>
<script src="//cdn.bootcss.com/axios/0.15.3/axios.min.js">script>
在 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
较好。
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 <div onClick={this.onClickDiv.bind(this)} />;
}
}
// good
class App extends React.Component {
constructor(props) {
super(props);
this.onClickDiv = this.onClickDiv.bind(this);
}
onClickDiv() {
// do stuff
}
render() {
return <div onClick={this.onClickDiv} />;
}
}
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 = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);
https://juejin.im/post/5c4a6fcd518825469414e062#heading-29
https://juejin.im/post/5cab64ce5188251b19486041#heading-6
https://juejin.im/post/5d548b83f265da03ab42471d
https://www.jianshu.com/p/333f390f2e84
https://yuchengkai.cn/docs/frontend/
github-myBlob