摘要
使用lighthouse进行性能检测,并对lighthouse提出的建议进行优化。
lighthouse
chrome的插件用不了,下了一个本地的。
## 下载
npm install -g lighthouse
## 运行
lighthouse 127.0.0.1:8080 --view --emulated-form-factor desktop -throttling-method=provided
主要优化
静态化
服务端渲染,“直出”页面,具有较好的SEO和首屏加载速度。主要还有以下的优点:
- 使用jsp模板语法(百度后发现是用Velocity模板语法)渲染页面,减少了js文件体积
- 减少了请求数量
- 因为不用等待大量接口返回,加快了首屏时间
可以尝试Vue的服务端渲染。首页目前有部分是用接口读取数据,然后用jq进行渲染,性能上应该不如Virtual DOM,不过内容不多。
图片懒加载
这是一个很重要的优化项。因为官网上有很多图片,而且编辑们上传文章图片的时候一般没有压缩,但是很多图片的体积都很大。还有一个轮播图,20张图标,最小的几十K,最大的两百多K。对于图片来源不可控的页面,懒加载是个很实用的操作,直接将首屏加载的资源大小加少了十几M。
图片压缩
对于来源可控,小图标等图片可以用雪碧图,base64等方法进行优化。目前只是用工具压缩了图片大小,后续可以考虑在webpack打包的时候生成雪碧图。
异步加载js
通过<script>
标签引入的js文件,可以设置defer
,async
属性让其异步加载,而不会阻塞渲染。defer
和async
的区别在于async
加载完就立即执行,没有考虑依赖,标签顺序等。而defer
加载完后会等它前面引入的文件执行完再执行。一般defer
用的比较多,async
只能用在那些跟别的文件没有联系的孤儿脚本上。
因为项目还是用webpack打包的,打包结果会将js文件以<script>
标签的形式注入到html模板中,那要如何给这个标签加上defer
属性呢?通过搜索,发现了webpack有一个html-webpack-plugin
插件的扩展插件script-ext-html-webpack-plugin
,可以通过这个插件给注入到html中的script
标签加上defer
属性。
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
plugins.push(new ScriptExtHtmlWebpackPlugin({
defaultAttribute: 'defer'
}))
异步加载css
没想到css也能异步加载,但这是lighthouse给出的建议。找了一下发现有以下两种方法:
一是通过js脚本在文档中插入<link>
标签
二是通过<link>
的media
属性
media
属性是媒体查询用的,用于在不同情况下加载不同的css。这里是将其设置为一个不适配当前浏览器环境的值,甚至是不能识别的值,浏览器会认为这个样式文件优先级低,会在不阻塞的情况加载。加载完成后再将media
设置为正常值,让浏览器解析css。
<link rel="stylesheet" href="//example.css" media="none" onload="this.media='all'">
这里用的是第二种方法。但是webpack注入到html中的外链css还没找到异步加载的方法。
preconnent
lighthouse建议对于接下来会访问的地址可以提前建立连接。一般有一下几种方式。
dns-prefetch
域名预解析
<link rel="dns-prefetch" href="//example.com">
preconnet
预连接
<link rel="preconnect" href="//example.com">
<link rel="preconnect" href="//cdn.example.com" crossorigin>
prefetch
预加载
<link rel="prefetch" href="//example.com/next-page.html" as="html" crossorigin="use-credentials">
<link rel="prefetch" href="library.js" as="script">
prerender
预渲染
<link rel="prerender" href="//example.com/next-page.html">
这四种层层递进,但是不要连接不需要的资源,反而损耗性能。我在页面上对某些资源用了preconnect
,但并没有明显的效果。应该对于在线小说,在线漫画这种场景预加载会更适用。
代码优化
lighthouse上显示主线程耗时最多的是样式和布局,所以对这部分进行优化。主要有一下几点:
- 去掉页面上用于布局的table,table本身性能较低,且维护性差,是一种过时的布局方案。
- 在去掉table布局的同时减少一些无意义的DOM元素,减少DOM元素的数量和嵌套。
- 减少css选择器的嵌套。用sass,less这种css预处理器很容易造成多层嵌套。优化前代码里最多的有七八层嵌套,对性能有一定影响。重构后不超过三层。
通过上面的重构后,样式布局和渲染时间从lighthouse上看大概减少了300ms。但样式和布局的时间还是最长的,感觉还有优化空间。
接下来是js代码的优化和重构。因为移除Vue框架,以及用服务端端直出,现在js代码已经减少了大部分。主要有以下几部分:
- 拆分函数,将功能复杂的函数拆分成小函数,让每个函数只做一件事。
- 优化分支结构,用对象
Object
,代替if...else
和switch...case
如下面这段代码,优化后变得更加简洁,也便于维护。
// 优化前
var getState = function (state) {
switch (state) {
case 1:
return 'up';
case 0:
return 'stay';
case 2:
return 'down';
}
}
// 优化后
var getState = function(state) {
var stateMap = {
1: 'up', 0: 'stay', 2: 'down'
}
return stateMap[state]
}
- 优化DOM操作
DOM操作如改变样式,改变内容可能会引起页面的重绘重排,是比较消耗性能的。网上也有很多优化jq操作的方法。
如将查询到的DOM使用变量存起来,避免重复查询。以及将多次DOM操作变成一次等。这里重点讲一下第二种。
常见的需求是渲染一个列表,如果直接在for循环里面append到父元素中,性能是非常差的。幸好原来的操作是将所有DOM用字符串拼接起来,再用html()
方法一次性添加到页面中。
还有另一种方法是使用文档碎片(fragment
)。通过document.createDocumentFragment()
可以新建一个fragment。向fragment
中appendChild
元素的时候是不会阻塞渲染进程的。最后将fragment
替换掉页面上的元素。将fragment
元素用appendChild
的方法添加到页面上时,实际上添加上去的是它内部的元素,也就是它的子元素。
var fragment = document.createDocumentFragment()
for (var i = 0; i < data.length; i++) {
var str = '<div>' + i + '</div>'
fragment.appendChild($(str)[0])
}
$('.container').append(fragment)
经过测试,在当前的场景下,使用fragment
的速度和html()
是差不多的,都是10ms左右。区别在于最后将fragment
添加到页面上$('.container').append(fragment)
这行代码仅仅花费1ms。也就是说,将fragment
插入页面时不会引起页面重绘重排,不会引起阻塞。
其他还没做的优化
lighthouse的提议还有以下几点:
- 使用新的图片格式,如webp。以或得更高的压缩率和更小图片体积。但是新的图片格式兼容性不是很好,故抛弃。
- 使用文本压缩,如gzip。js,css和html文件都可以使用gzip压缩,webpack也有对应的配置项。有时候费尽脑汁去考虑怎么减小代码体积都不如gzip压缩来的有效果。但这个需要在服务器配置。
总结
个人认为,对首屏加载速度影响最大的还是资源大小,请求数量,请求速度等。代码方面,前端一般很难写出严重影响速度的代码。减小资源大小,可以用各种压缩,懒加载,预加载,异步加载等方法。减少请求数量可以使用雪碧图,搭建node中台将多个请求合并成一个等。对于官网这种项目,最好使用服务端渲染,首屏快之外,也有利于SEO。