性能优化
这本身就是一个大问题,会直接被问“如何提高前端性能”。而且这些性能优化的事儿,已经远远超出了 JS 知识的范畴,只不过面试会问到,我必须告诉大家一些基本的回答方式。
这种问题没有正确答案,特别是不同层面关注的点不一样
优化原则和方向
原则
- 多使用内存、缓存或者其他方法
- 减少 CPU 计算、较少网络
方向
- 加载页面和静态资源
- 页面渲染
加载资源优化
- 静态资源的压缩合并(JS代码压缩合并、CSS代码压缩合并、雪碧图)
- 静态资源缓存(资源名称加 MD5 戳)
- 使用 CND 让资源加载更快
- 使用 SSR 后端渲染,数据直接突出到 HTML 中
渲染优化
- CSS 放前面 JS 放后面
- 懒加载(图片懒加载、下拉加载更多)
- 减少DOM 查询,对 DOM 查询做缓存
- 减少DOM 操作,多个操作尽量合并在一起执行(
DocumentFragment) - 节流和防抖
- 尽早执行操作(
DOMContentLoaded)
详细解说
静态资源的压缩合并
如果不合并,每个都会走一遍之前介绍的请求过程
html
<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>如果压缩了,就只走一遍请求过程
html
<script src="abc.js"></script>静态资源缓存
通过连接名称控制缓存
html
<script src="abc_1.js"></script>只有内容改变的时候,链接名称才会改变
html
<script src="abc_2.js"></script>使用 CND 让资源加载更快
html
<script src="https://cdn.bootcss.com/zepto/1.0rc1/zepto.min.js"></script>使用 SSR 后端渲染
如果提到 Vue 和 React 时,可以说一下
CSS 放前面 JS 放后面
将浏览器渲染的时候,已经提高
懒加载
一开始先给为 src 赋值成一个通用的预览图,下拉时候再动态赋值成正式的图片
html
<img src="preview.png" data-realsrc="abc.png"/>DOM 查询做缓存
两端代码做一下对比
js
// 不缓存 DOM 查询结果
for (let = 0; i < document.getElementsByTagName('p').length; i++) {
// 每次循环,都会计算 length ,频繁进行 DOM 查询
}
// 缓存 DOM 查询结果
const pList = document.getElementsByTagName('p')
const length = pList.length
for (let i = 0; i < length; i++) {
// 缓存 length ,只进行一次 DOM 查询
}总结:DOM 操作,无论查询还是修改,都是非常耗费性能的,尽量减少
合并 DOM 插入
DOM 操作是非常耗费性能的,因此插入多个标签时,先插入 Fragment 然后再统一插入DOM
js
const listNode = document.getElementById('list')
// 创建一个文档片段,此时还没有插入到 DOM 树中
const frag = document.createDocumentFragment()
// 执行插入
for(let x = 0; x < 10; x++) {
const li = document.createElement("li")
li.innerHTML = "List item " + x
frag.appendChild(li)
}
// 都完成之后,再插入到 DOM 树中
listNode.appendChild(frag)尽早执行操作
js
window.addEventListener('load', function () {
// 页面的全部资源加载完才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded', function () {
// DOM 渲染完即可执行,此时图片、视频还可能没有加载完
})节流和防抖
例如要在文字改变时触发一个 change 事件,通过 keyup 来监听。使用防抖。
js
const textarea = document.getElementById('text')
let timer
textarea.addEventListener('keyup', () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
// 触发 change 事件
// 清空定时器
timer = null
}, 100)
})可以把防抖时间单独抽离出来实现
js
// 手写防抖
function debounce(fn, delay = 200) {
// timer 在闭包中
let timer = null
// 返回一个函数
return function() {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments) // 透传 this 和函数参数
timer = null // 触发过了,重新计时
}, delay)
}
}
const textarea = document.getElementById('text')
textarea.addEventListener('keyup', debounce(() => {
// 触发 change 事件
}))在拖拽时,随时要检测当前的位置信息(如是否覆盖了目标元素的位置),可用节流。
js
// <p id="p1" draggable="true">拖动我!</p>
const p1 = document.getElmentById('p1')
p1.addEventListener('drag', e => {
// 这样会打印很频繁,如果在其中再做一些 DOM 查询,那就出现卡顿
console.log('鼠标位置', e.offsetX, e.offsetY)
})使用节流
js
// 手写节流
function throttle(fn, delay = 100) {
// timger 在闭包中
let timer = null
// 返回一个函数
return function(){
//当我们发现这个定时器存在时,则表示定时器已经在运行中,还没到该触发的时候,则 return
if (timer) {
return
}
// 定时器不存在了,说明已经触发过了,重新计时
timer = setTimeout(()=>{
fn.apply(this, arguments) // 透传 this 和函数参数
timer = null // 清空定时器
}, delay)
}
}
// 再次实现
p1.addEventListener('drag', throttle(e => {
console.log('鼠标位置', e.offsetX, e.offsetY)
})
讨论区
欢迎留下想法与补充