漫谈前端性能优化
url加载到页面加载完成发送了什么?
这是一个互联网从业者时常必须关注的问题。
作为前端关注浏览器。这个阶段,就是性能可以优化的时间。
前端性能优化有两个大方向:
现代浏览器的工作方式:推荐阅读《How Browser Works》
https://www.cnblogs.com/zjstar12/archive/2012/01/12/2320408.html
前端在这块能做的事情比较少,
打开淘宝的网页源码,会发现有些标签预先获取一些dns。
淘宝首页加载时,这些域名的dns结果已经拿回来了。
推荐阅读:图解TCP/IP,图解http,http权威指南,
在软件层面来说,最底层就是ip协议。ip最主要做的就是寻址。不负责数据内容。
TCP负责数据的完整性和有序。一旦发生丢包等,立即补发。(慢)
UDP和TCP是一层的协议,只发数据,但不管别的。主要使用场景:语音,视频等。(dns也是用的)
http:每次建立连接之前都会触发三次握手。了解一下:
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(seq=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
解决文件:图片绝对是大头。
HTTP 压缩就是以缩小体积为目的,对 HTTP 内容进行重新编码的过程 Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。 根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大。反之亦然。基本上来说,Gzip都是服务器干的活,比如nginx
更多详情请见本公众号文章《http网络编程》
协议头:值
协议头:值
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。但在上面我们提到过,cache-control的优先级更高。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当客户端本地时间被修改以后,服务器与客户端时间偏差变大以后,就会导致缓存混乱。于是发展出了Cache-Control。
Cache control 浏览器没有,服务器给你文件。它是一个相对时间,例如Cache-Control:3600,代表着资源的有效期是3600秒。由于是相对时间,并且都是与客户端时间比较,所以服务器与客户端时间偏差也不会导致问题。Cache-Control与Expires可以在服务端配置同时启用或者启用任意一个,同时启用的时候Cache-Control优先级高。
max-age 指定一个时间长度,在这个时间段内缓存是有效的,单位是s。例如设置 Cache-Control:max-age=31536000,也就是说缓存有效期为(31536000 / 24 / 60 * 60)天,第一次访问这个资源的时候,服务器端也返回了 Expires 字段,并且过期时间是一年后。在没有禁用缓存并且没有超过有效时间的情况下,再次访问这个资源就命中了缓存,不会向服务器请求资源而是直接从浏览器缓存中取。
过期时间没到:直接200。
实际上这里是没有发送请求(from cache)。
若未命中强缓存(如果已经过期了),浏览器也不会马上加载。
浏览器会将请求发送至服务器。服务器根据http头信息中的Last-Modify/If-Modify-Since或Etag/If-None-Match来判断是否命中协商缓存。如果命中,则http返回码为304,浏览器从缓存中加载资源。
如果没有修改,则返回304
与Last-Modify/If-Modify-Since不同的是,Etag/If-None-Match返回的是一个校验码(ETag: entity tag)。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化*。ETag值的变更则说明资源状态已经被修改。服务器根据浏览器上发送的If-None-Match值来判断是否命中缓存。
如果都不行,那只能重载了!
普通文件上线不久是替换文件么?如果这样,用户可能在时间段内无法请求内容。
解决方案:
<script src="xx.js?_v=1.01" />
<script src="xx.js?_v=文件的哈希值" />
webpack-fis可以计算哈希值。
先上线模版html,导致加载的是旧的js会报错。
如果你的js哈希值变了。那么就会放弃旧的缓存,请求新的js资源。
因此:
对于自己的代码,怎么优化?
我们打开一个过往项目,执行npm run build:
会发现gz压缩后的大小已经小了很多。但还是很大。如何去分析自己的项目?
vue ui可以看自己的项目。
npm i webpack-bundle-analyzer -s
然后在webpack 配置里写:
const BundleAnalyzer=require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.export={
//...
plugins:[
new BundleAnalyzer(),
// ...
]
}
在开发环境中看http://localhost:8888,会发现:
Iview 的样式占了大头。那你就考虑是不是不要全局引入ivew.css了。
简单说:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
我们在vue-router里使用类似这样的方式2引入组件:
在react里面也是类似的。
传统的spa应用。首页白屏是非常常见的。对于seo来说也是非常不友好。根治这个问题,解决方案就是ssr。
一个同构应用,需要服务端(server和next两个入口)
本质就是在服务端把渲染的活给干了·
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)
关于nuxt.js我将在另外一篇文章中阐述。
关键词:renderToString。属于react-dom的一个api。
对于jsx语法,在服务端必须印图babel。
// 注意这是服务端。
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)
ssr理论必读。
不一定都很合适。只能说在不同环境下有不同的配置。
尽量减少 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 的域
https://developer.mozilla.org/zh-CN/docs/Web/API/Performance
直接在浏览器控制台输入
performance.getEntriesByType('navigation')
我们可以通过这个api来进行一个统计。
比如说,以下两个值之差就是dns寻找时间。
重定向耗时:redirectEnd - redirectStart DNS查询耗时 :domainLookupEnd - domainLookupStart TCP链接耗时 :connectEnd - connectStart HTTP请求耗时 :responseEnd - responseStart 解析dom树耗时 : domComplete - domInteractive 白屏时间 :responseStart - navigationStart DOMready时间 :domContentLoadedEventEnd - navigationStart onload时间:loadEventEnd - navigationStart,也即是onload回调函数执行的时间。
lighthouse是一款由谷歌团队开发的一款开源的网站性能测评浏览器扩展程序。
目前测试项包括页面性能
、PWA
、可访问性(无障碍)
、最佳实践
、SEO
。Lighthouse会对各个测试项的结果打分,并给出优化建议,这些打分标准和优化建议可以视为Google的网页最佳实践。
使用后可以看到网站性能。
这是面试的重点。以班车为例子。
什么叫防抖?尝试手写一下。
设想你进行百度搜索,业务逻辑是输入每个文字(onkeyup),都弹出搜索建议。
但是这样每次调优都很很痛苦。我还没输入完成你就弹建议。不符合操作习惯。
所以防抖的核心思想是:频繁调用一个请求时,等待最后一个操作结束之后。才进行操作。
<input type="text" id="aaa">
<script>
const aaa=document.querySelector('#aaa');
aaa.addEventListener('keyup',(e)=>{
// 假设这时就发送了请求。
console.log(`你查找的是不是:${aaa.value}`)
})
</script>
这段代码缺陷明显。我想输入111,还没输完就累计请求了3次。所以需要防抖一下。
const aaa = document.querySelector('#aaa');
// 防抖函数
const debounce = function(fn, interval){
if(this.timer){
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
fn();
}, interval)
}
aaa.addEventListener('keyup', (e) => {
debounce(()=>console.log(`你查找的是不是:${aaa.value}`),1000)
});
最后只执行了一次。timer以闭包的形式存下来了。
与防抖对应的就是节流,也就是说,我每隔一定的时间间隔,发车。
// 节流
const throttle=(fn,interval)=>{
const lastTime=0;
return function(...args){
let now=new Date();
if(now-lastTime>interval){
fn.apply(this,args);
lastTime=now;
}
}
}
这样就省去了定时器。
防抖和节流在数据校验上很有用。没输完就校验是毫无意义的。
回流:当你改变一个dom的几何尺寸。浏览器就会重新计算,导致其它标签外观变化。甚至触发蝴蝶效应。
这就是回流。
相反,你只是改了一个小div的颜色,其它属性没有变化,这就是重绘。
回流必定导致重绘,但重绘未必导致节流。
懒加载通常会用data-url存放你想加载的图片。通过一定的条件触发加载。
给你1w条数据,怎么显示?
在一个长列表(虚拟列表)中,假设我有1w条,触发dom结构是非常痛苦的。
本质上就是和分页类似。
实际上只渲染可见的(前后2-3屏)。超过这个范围:触发新老节点替换渲染。
框架实现:react-virtualized 可以研究下源码实现。