# 前端性能优化

# 一、资源压缩与合并

主要包括这些方面:html压缩、css 压缩、js的压缩和混乱和文件合并。资源压缩可以从文件中去掉多余的字符,比如回车、空格。你在编辑器中写代码的时候,会使用缩进和注释,这些方法无疑会让你的代码简洁而且易读,但它们也会在文档中添加多余的字节。

# 1.html压缩

html代码压缩就是压缩这些在文本文件中有意义,但是在HTML中不显示的字符,包括空格,制表符,换行符等,还有一些其他意义的字符,如HTML注释也可以被压缩。

如何进行html压缩:

  • nodejs 提供了html-minifier工具
  • 后端模板引擎渲染压缩

# 2.css代码压缩

css代码压缩简单来说就是无效代码删除和css语义合并

如何进行css压缩:

  • 使用html-minifier工具
  • 使用clean-css对css压缩

# 3.js的压缩和混乱

js的压缩和混乱主要包括以下这几部分:无效字符的删除、剔除注释、代码语义的缩减和优化、代码保护(代码逻辑变得混乱,降低代码的可读性,这点很重要)

如何进行js的压缩和混乱

  • 使用在线网站进行压缩(开发过程中一般不用)
  • 使用html-minifier工具
  • 使用uglifyjs2对js进行压缩

其实css压缩与js的压缩和混乱比html压缩收益要大得多,同时css代码和js代码比html代码多得多,通过css压缩和js压缩带来流量的减少,会非常明显。所以对大公司来说,html压缩可有可无,但css压缩与js的压缩和混乱必须要有!

# 4.文件合并

不合并请求有以下缺点:

  • 文件与文件之间有插入的上行请求,增加了N-1个网络延迟
  • 受丢包问题影响更严重
  • keep-alive方式可能会出现状况,经过代理服务器时可能会被断开,也就是说不能一直保持keep-alive的状态

压缩合并css和js可以减少网站http请求的次数,但合并文件可能会带来问题:首屏渲染和缓存失效问题。那该如何处理这问题呢?公共库合并和不同页面的合并。

如何进行文件合并?

  • 使用nodejs实现文件合并(gulp、fis3)

# 二、脚本异步加载

异步加载的三种方式——async和defer、动态脚本创建。

async和defer的相同特点是:

  • 加载脚本时不阻塞页面渲染。
  • 使用这个属性的脚本中不能调用document.write方法。
  • 可以只写属性名,不写属性值。

# 1、async方式

<script type="text/javascript" src="xxx.js" async></script>
1

async特点:

  • H5新增属性。
  • 脚本在下载后立即执行,同时会在window的load事件之前执行,所以有可能出现脚本执行顺序被打乱的情况。

# 2、defer方式

<script type="text/javascript" src="xxx.js" defer></script>
1

defer特点:

  • H4属性。
  • 脚本在页面解析完之后,按照原本的顺序执行,同时会在document的DOMContentLoaded之前执行。

# 3、动态创建script标签

在还没定义defer和async前,异步加载的方式是动态创建script,通过window.onload方法确保页面加载完毕再将script标签插入到DOM中,具体代码如下:

function addScriptTag(src){  
    var script = document.createElement('script');  
    script.setAttribute("type","text/javascript");  
    script.src = src;  
    document.body.appendChild(script);  
}  
window.onload = function(){  
    addScriptTag("js/index.js");  
}
1
2
3
4
5
6
7
8
9

# 4、对比

正常script、带async的script和带defer的script 对HTML解析的影响。

image

async和defer的区别:

  • async是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关。
  • defer是在HTML解析完之后才会执行,如果是多个,按照加载的顺序依次执行。

# 三、浏览器缓存

参考浏览器缓存

# 四、使用CDN

大型Web应用对速度的追求并没有止步于仅仅利用浏览器缓存,因为浏览器缓存始终只是为了提升二次访问的速度,对于首次访问的加速,我们需要从网络层面进行优化,最常见的手段就是CDN(Content Delivery Network,内容分发网络)加速。通过将静态资源(例如javascript,css,图片等等)缓存到离用户很近的相同网络运营商的CDN节点上,不但能提升用户的访问速度,还能节省服务器的带宽消耗,降低负载。

image

CDN的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。 最简单的CDN网络由一个DNS 服务器和几台缓存服务器就可以组成,当用户输入URL按下回车,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器,然后将得到全局负载均衡设备的IP地址,用户向全局负载均衡设备发送内容访问请求,全局负载均衡设备将实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。

# 五、DNS预解析

资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。 通过 DNS 预解析来告诉浏览器未来我们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就可以尽快地完成 DNS 解析。例如,我们将来可从 example.com 获取图片或音频资源,那么可以在文档顶部的 <head> 标签中加入以下内容:

<link rel="dns-prefetch" href="//example.com">
1

当我们从该 URL 请求一个资源时,就不再需要等待 DNS 的解析过程。该技术对使用第三方资源特别有用。通过简单的一行代码就可以告知那些兼容的浏览器进行 DNS 预解析,这意味着当浏览器真正请求该域中的某个资源时,DNS 的解析就已经完成了,从而节省了宝贵的时间。 另外需要注意的是,浏览器会对a标签的href自动启用DNS Prefetching,所以a标签里包含的域名不需要在head中手动设置link。但是在HTTPS下不起作用,需要meta来强制开启功能。这个限制的原因是防止窃听者根据DNS Prefetching推断显示在HTTPS页面中超链接的主机名。下面这句话作用是强制打开a标签域名解析

<meta http-equiv="x-dns-prefetch-control" content="on">
1

image

# 六、服务端渲染技术

如果是SPA ⾸屏SSR就是性能优化的重要⼀环,用 nuxt(vue服务端渲染框架) 和 next(React服务端渲染框架)

# vue服务端渲染

const Vue = require('vue')
// 创建⼀个express应⽤
const server = require('express')()
// 提取出renderer实例
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
	// 编写Vue实例(虚拟DOM节点)
	const app = new Vue({
		data: {
			url: req.url
		},
		// 编写模板HTML的内容
		template: `<div>访问的 URL 是: {{ url }}</div>`
	})
	// renderToString 是把Vue实例转化为真实DOM的关键⽅法
	renderer.renderToString(app, (err, html) => {
		if (err) {
			res.status(500).end('Internal Server Error')
			return
		}
		// 把渲染出来的真实DOM字符串插⼊HTML模板中
		res.end(`
		 <!DOCTYPE html>
		 <html lang="en">
		 <head><title>Hello</title></head>
		 <body>${html}</body>
		 </html>
		`)
	})
})
server.listen(8080)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# react服务端渲染

import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import App from './App'
const app = express()
// renderToString 是把虚拟DOM转化为真实DOM的关键⽅法
const RDom = renderToString( < App / > )
// 编写HTML模板,插⼊转化后的真实DOM内容
const Page = `
	 <html>
		 <head>
		 	<title>test</title>
		 </head>
		 <body>
			 <span>ssr </span>
			 ${RDom}
		 </body>
	 </html>
`
app.get('/index', function(req, res) {
	res.send(Page)
})
// 配置端⼝号
const server = app.listen(8000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 七、代码层面的优化

# 1、雅虎军规

尽量减少 HTTP 请求个数——须权衡
使⽤ CDN(内容分发⽹络)
为⽂件头指定 Expires 或 Cache-Control ,使内容具有缓存性。
避免空的 src 和 href
使⽤ gzip 压缩内容
把 CSS 放到顶部
把 JS 放到底部
避免使⽤ CSS 表达式
将 CSS 和 JS 放到外部⽂件中
减少 DNS 查找次数
精简 CSS 和 JS
避免跳转
剔除重复的 JS 和 CSS
配置 ETags
使 AJAX 可缓存
尽早刷新输出缓冲
使⽤ GET 来完成 AJAX 请求
延迟加载
预加载
减少 DOM 元素个数
根据域名划分⻚⾯内容
尽量减少 iframe 的个数
避免 404
减少 Cookie 的⼤⼩
使⽤⽆ cookie 的域
减少 DOM 访问
开发智能事件处理程序
⽤ 代替 @import
避免使⽤滤镜
优化图像
优化 CSS Spirite
不要在 HTML 中缩放图像——须权衡
favicon.ico要⼩⽽且可缓存
保持单个内容⼩于25K
打包组件成复合⽂本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 2、防抖和节流

参考

# 3、图片懒加载(lazy-load)

// 获取所有的图⽚标签
 const imgs = document.getElementsByTagName('img')
 // 获取可视区域的⾼度
 const viewHeight = window.innerHeight ||
document.documentElement.clientHeight
 // num⽤于统计当前显示到了哪⼀张图⽚,避免每次都从第⼀张图⽚开始检查是否露出
 let num = 0
 function lazyload(){
     for(let i=num; i<imgs.length; i++) {
         // ⽤可视区域⾼度减去元素顶部距离可视区域顶部的⾼度
         let distance = viewHeight - imgs[i].getBoundingClientRect().top
         // 如果可视区域⾼度⼤于等于元素顶部距离可视区域顶部的⾼度,说明元素露出
         if(distance >= 0 ){
             // 给元素写⼊真实的src,展示图⽚
             imgs[i].src = imgs[i].getAttribute('data-src')
             // 前i张图⽚已经加载完毕,下次从第i+1张开始检查是否露出
             num = i + 1
         }
     }
 }
 // 监听Scroll事件
 window.addEventListener('scroll', lazyload, false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

getBoundingClientRect() 的top、left、right、bottom

image

# 4、减少重排和重绘

参考重排和重绘

# 5、Vue的性能优化

1、v-if vs v-show 初始性能 VS 频繁切换性能

2、和渲染⽆关的数据,不要放在data上,data也不要嵌套过多层

# 6、React的性能优化

1、只传递需要的props,不要随便 <Component {...props} />

2、数组渲染用key,⽆状态组件

3、pureComponent shouldComponentUpdate 渲染时机

4、少在render中绑定事件

5、redux + reselect data扁平化

每当store发⽣改变的时候,connect就会触发重新计算,为了减少重复的不必要计算,减少⼤型项⽬的性能开⽀,需要对selector函数做缓存。推荐使⽤reactjs/reselect, 缓存的部分实现代码如下。

6、⻓列表 react-virtualized

原理:只渲染可⻅的.

优化前:

<div className="list">
 {this.list.map(this.renderRow.bind(this))}
</div>

renderRow(item) {
 return (
     <div key={item.id} className="row">
         <div className="image">
            <img src={item.image} alt="" />
         </div>
         <div className="content">
             <div>{item.name}</div>
             <div>{item.text}</div>
         </div>
     </div>
 );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

优化后:

import { List } from "react-virtualized";

// 改为
const listHeight = 600;
const rowHeight = 50;
const rowWidth = 800;

<div className="list">
     <List
     width={rowWidth}
     height={listHeight}
     rowHeight={rowHeight}
     rowRenderer={this.renderRow.bind(this)}
     rowCount={this.list.length} />
</div>

renderRow({ index, key, style }) {
 return (
     <div key={key} style={style} className="row">
         <div className="image">
            <img src={this.list[index].image} alt="" />
         </div>
         <div className="content">
             <div>{this.list[index].name}</div>
             <div>{this.list[index].text}</div>
         </div>
     </div>
 );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 7、骨架屏

image

使用 vue-skeleton-webpack-plugin 插件.

# 具体使用

1、在 webpack.dev.conf.js和webpack.prod.conf.js中引入插件

const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
function resolve(dir) {
    return path.join(__dirname, '..', dir)
}

plugins: [
    new SkeletonWebpackPlugin({
        webpackConfig: {
            entry: {
                app: resolve('./src/entry-skeleton.js')
            }
        }
    })
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14

2、创建骨架屏组件 src/skeleton.vue

<template>
    <div class="skeleton-wrapper">
        <section class="skeleton-block">
            <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg==">
            <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg==">
        </section>
    </div>
</template>

<script>
    export default {
        name: 'skeleton'
    };
</script>

<style scoped>
    .skeleton-block {
        display: flex;
        flex-direction: column;
        padding-top: 8px;
    }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

3、创建骨架屏组件的入口文件src/entry-skeleton.js

import Vue from 'vue'
// 创建的骨架屏 Vue 实例
import skeleton from './skeleton';
export default new Vue({
    components: {
        skeleton
    },
    template: '<skeleton />'
});
1
2
3
4
5
6
7
8
9

# 实现一个骨架屏插件

参考

# 8、使用CSS动画,不要用JS动画

用CSS动画替代js模拟动画的好处是:不占用js主线程;可以利用硬件加速;浏览器可对动画做优化。但CSS动画有时会出现卡顿现象。

使用CSS3动画造成页面的不流畅和卡顿问题,其潜在原因往往还是页面的回流和重绘,减少页面动画元素对其他元素的影响是提高性能的根本方向。

(1)设置动画元素 position 样式为absolute或 fixed,可避免动画的进行对页面其它元素造成影响,导致其重绘和重排的发生;

(2)避免使用margin,top,left,width,height等属性执行动画,用 transform 进行替代;因为transform属性不会触发浏览器的 repaint,而 top 和 left 会一直触发 repaint。为什么 transform 没有触发 repaint 呢?因为,transform 动画由GPU控制,支持硬件加速,并不需要软件方面的渲染。

用CSS3动画替代JS模拟动画的好处:

  • 不占用JS主线程;
  • 可以利用硬件加速;
  • 浏览器可对动画做优化(元素不可见时不动画减少对FPS影响)

# 9、避免频繁操作DOM元素

当需要很多的插入操作和改动,使用下面的代码会很有问题

  var ul = document.getElementById("ul");
  for (var i = 0;i < 20;i++){
    var li = document.creatElement("li");
    ul.appendChild(li);
  }
1
2
3
4
5

由于每一次对文档的插入都会引起重新渲染(计算元素的尺寸、显示背景、内容等),所以进行多次插入操作使得浏览器发生了多次渲染,效率比较低。这是我们提倡减少页面的渲染来提高DOM操作的效率的原因。

# createDocumentFragment

createDocumentFragment()方法,是用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点,它可以包含各种类型的节点,在创建之初是空的。它有一个很实用的特点,当请求把一个createDocumentFragment 节点插入文档树时,插入的不是createDocumentFragment 自身,而是它所有的子孙节点。这个特性使得createDocumentFragment 成了占位符,暂时存放那些一次插入文档的节点。

另外,当需要添加多个dom元素时,如果先将这些元素添加到createDocumentFragment 中,再统一将createDocumentFragment 添加到页面。因为文档片段存在于内存中,并不在DOM中,所以将子元素插入文档片段中不会引起回流(对元素位置和几何上的计算),因此,使用DocumentFragment可以起到性能优化的作用。可以将上面的代码改成下面的

  var ul = documen.getElementById("ul");
  var fragment = document.createDocumentFragment();
  for (var i = 0;i < 20;i++){
    var li = document.createElement("li");
    li.innerHtml =" index: " + i;
    fragment.appendChild(li);
  }
  ul.appendChild(fragment);
1
2
3
4
5
6
7
8

# 虚拟列表技术

插入几万个 DOM,如何实现页面不卡顿?

对于这道题目来说,首先我们肯定不能一次性把几万个 DOM 全部插入,这样肯定会造成卡顿,所以解决问题的重点应该是如何分批次部分渲染 DOM。大部分人应该可以想到通过 requestAnimationFrame 的方式去循环的插入 DOM,其实还有种方式去解决这个问题:虚拟滚动(virtualized scroller)。

这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。

image

从上图中我们可以发现,即使列表很长,但是渲染的 DOM 元素永远只有那么几个,当我们滚动页面的时候就会实时去更新 DOM,react可以使用react-virtualized来实现虚拟列表技术。

# 其它可优化的点

  • 设置DOM元素的display属性为none再操作该元素
  • 复制DOM元素到内存中再对其进行操作
  • 用局部变量缓存样式信息从而避免频繁获取DOM数据
  • 合并多次DOM操作

# 八、首屏加载优化方案

  • Vue-Router路由懒加载(利用Webpack的代码切割)
  • 使用CDN加速,将通用的库从vendor进行抽离
  • Nginx的gzip压缩
  • Vue异步组件
  • 服务端渲染SSR
  • 如果使用了一些UI库,采用按需加载
  • Webpack开启gzip压缩
  • 如果首屏为登录页,可以做成多入口
  • Service Worker缓存文件处理
  • 使用link标签的rel属性设置 prefetch(这段资源将会在未来某个导航或者功能要用到,但是本资源的下载顺序权重比较低,prefetch通常用于加速下一次导航)、preload(preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度)
Last Updated: 7/20/2020, 3:51:57 PM