前端性能优化之首屏加载【懒加载,监听元素视口可见,骨架屏优化白屏时长,长列表虚拟滚动,压缩图片,Tree shaking,base64】
懒/动态加载:按需/运动加载。
目录
webpackChunkName: "dialogInfo"
Tree shaking:消除无用的 JS 代码,减少代码体积
requestAnimationFrame 制作动画:刷新频率与显示器的频率保持一致
setTimeout/setInterval 属于 JS引擎
requestAnimationFrame 属于 GUI引擎
JS引擎与GUI引擎是互斥的: GUI 引擎在渲染时会阻塞 JS 引擎的计算
requestAnimationFrame 刷新频率是固定且准确的
setTimeout/setInterval 定时器仍会在后台执行动画任务,
requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停
b.IntersectionObserver交叉观察器:目标元素与父/视口产生交叉区
使用:this.$nextTick(()=>{DOM更新后观察})
图片的 二进制数据 的 字符串 表示形式,图片大小->原文件的 4/3
类似html中嵌入的文字,因此无需单独文件(无法缓存),从而无需请求
小图片(小于 100KB),如小图标、图像按钮、装饰性背景图;
a.FileReader 的 readAsDataURL()
懒/动态加载:按需/运动加载
路由懒加载
SPA :一个路由对应一个页面
把所有页面打包成一个文件
如果不做处理,项目打包后,会把所有页面打包成一个文件,当用户打开首页时,会一次性加载所有的资源,造成首页加载很慢,降低用户体验
懒加载前提:ES6动态地加载模块——import()
调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中
——摘自《webpack——模块方法》的import()小节
webpackChunkName:分离到单独的 chunk
要实现懒加载,就得先将进行懒加载的子模块分离出来,打包成一个单独的文件
webpackChunkName 作用是 webpack 在打包的时候,对异步引入的库代码(lodash)进行代码分割时,设置代码块的名字。webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中
// 通过webpackChunkName设置分割后代码块的名字
const Home = () => import(/* webpackChunkName: "home" */ "@/views/home/index.vue");
const MetricGroup = () => import(/* webpackChunkName: "metricGroup" */ "@/views/metricGroup/index.vue");
…………
const routes = [
{
path: "/",
name: "home",
component: Home
},
{
path: "/metricGroup",
name: "metricGroup",
component: MetricGroup
},
…………
]
组件懒加载
webpackChunkName: "dialogInfo"
home 页面 和 about 页面,都引入了 dialogInfo 弹框组件,该弹框不是一进入页面就加载,而是需要用户手动触发后才展示出来
<script>
const dialogInfo = () => import(/* webpackChunkName: "dialogInfo" */ '@/components/dialogInfo');
export default {
name: 'homeView',
components: {
dialogInfo
}
}
</script>
适用场景
体积大
该页面的 JS 文件体积大,导致页面打开慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度(比如首页)
非首屏
该组件不是一进入页面就展示,需要一定条件下才触发(比如弹框组件)
复用性高
该组件复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的 JS 文件大小(比如表格组件、图形组件等)
Tree shaking:消除无用的 JS 代码,减少代码体积
项目中只使用了 targetType 方法,但未使用 deepClone 方法,项目打包后,deepClone 方法不会被打包到项目里
// util.js
export function targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export function deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
原理:ES6模块静态分析
静态分析就是不需要执行代码,就可以从字面量上对代码进行分析。ES6之前的模块化,比如 CommonJS 是动态加载,只有执行后才知道引用的什么模块,就不能通过静态分析去做优化
只适用于函数式编程
无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效
// util.js
export default {
targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
},
deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
};
// 引入并使用
import util from '../util';
util.targetType(null)
骨架屏优化白屏时长
虚拟列表:大量数据
长列表虚拟滚动:只渲染可视区,非可见区域的不渲染
计算出 totalHeight 列表总高度,并在触发时滚动事件时根据 scrollTop 值不断更新 startIndex 以及 endIndex ,以此从列表数据 listData 中截取对应元素
虚拟滚动插件
虚拟滚动的插件有很多,比如 vue-virtual-scroller、vue-virtual-scroll-list、react-tiny-virtual-list、react-virtualized
Web Worker 优化长任务
由于浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况
查看页面的长任务:
打开控制台,选择 Performance 工具,点击 Start 按钮,展开 Main 选项,会发现有很多红色的三角,这些就属于长任务(长任务:执行时间超过50ms的任务)
适用:当任务的运算时长 - 通信时长 > 50ms
Time 是这个资源的通信时长(也叫加载时长)
requestAnimationFrame 制作动画:刷新频率与显示器的频率保持一致
可以解决用 setTimeout/setInterval 制作动画卡顿的情况
优先级
setTimeout/setInterval 属于 JS引擎
requestAnimationFrame 属于 GUI引擎
JS引擎与GUI引擎
是互斥的: GUI 引擎在渲染时会阻塞 JS 引擎的计算
时间
requestAnimationFrame 刷新频率是固定且准确的
setTimeout/setInterval 是宏任务
根据事件轮询机制,其他任务会阻塞或延迟js任务的执行,会出现定时器不准的情况
性能
当页面被隐藏或最小化时,
setTimeout/setInterval 定时器仍会在后台执行动画任务,
requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停
列表(以图片为例)
src属性
src :自动发送请求、下载
data-src :暂存 src 的值
HTML5 自定义属性 data-xxx 主要用于在 DOM 元素中存储额外的信息,而不会自动请求资源
在图片出现在屏幕可视区域的时候,再将 data-xxx 的值重新赋值到 img 的 src 属性即可
<img src="" alt="" data-src="./images/1.jpg">
<img src="" alt="" data-src="./images/2.jpg">
动态裁剪:在图片url地址上动态添加参数
懒加载/视口可见元素
(Load On Demand)延迟加载、按需加载
可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。
A.浏览器原生监听器
节流和防抖能减少执行次数,但不能消除
scroll
频繁触发的本质
IntersectionObserver
在浏览器的层面处理可见性检测,能够批量处理多个观察者的状态变化,减少计算和触发的频率
IntersectionObserver
2016 年( Chrome 51 和 Firefox 55),因此scroll 更兼容
a.scroll +视口顶部距离
使用场景:检测滚动位置
存储初始边界值,滚动会更改相应距离位置
scroll回调放在 requestAnimationFrame
中,以在浏览器下次重绘前执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.scrollBox {
width: 100%;
height: 300px;
background-color: #ccc;
overflow-y: auto;
}
.target {
width: 60px;
height: 60px;
background-color: red;
}
</style>
</head>
<body>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<div class="scrollBox">
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<div class="target"></div>
</div>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<h1>孙悟空</h1>
<script>
const scrollBoxDom = document.querySelector('.scrollBox');
const targetDom = document.querySelector('.target');
let lastKnownScrollPosition = 0;
let ticking = false;
// 计算边界值
const boundaryValue = targetDom.getBoundingClientRect().top - scrollBoxDom.getBoundingClientRect().top - scrollBoxDom.offsetHeight;
function handleScroll(scrollPos) {
if (scrollPos > boundaryValue) {
console.log('在可视区域');
scrollBoxDom.style.backgroundColor = 'pink';
} else {
console.log('不在可视区域');
scrollBoxDom.style.backgroundColor = '#ccc';
}
}
// 防抖函数
function debounce(func, wait) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// 创建防抖后的处理函数
const debouncedScroll = debounce(function () {
if (!ticking) {
window.requestAnimationFrame(function () {
handleScroll(lastKnownScrollPosition);
ticking = false;
});
ticking = true;
}
}, 100); // 100ms 的防抖时间
scrollBoxDom.addEventListener('scroll', function () {
lastKnownScrollPosition = scrollBoxDom.scrollTop;
debouncedScroll();
});
</script>
</body>
</html>
- 图片的真实路径则设置在data-src属性中,
- 绑定 window 的
scroll
事件,对其进行事件监听。 -
//节流 window.addEventListener('scroll', throttle(lazyload, 200))
- 在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,
- 如果图片在可视区内将图片的 src 属性设置为data-src的值
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lazyload</title>
<style>
.image-item {
display: block;
margin-bottom: 50px;
height: 200px;//一定记得设置图片高度
}
</style>
</head>
<body>
<img src="./img/default.png" data-src="./img/1.jpg" />
...
<img src="./img/default.png" data-src="./img/10.jpg" />
<script>
function lazyload() {
let viewHeight = document.body.clientHeight //获取可视区高度
//用属性选择器返回属性名为data-src的img元素列表
let imgs = document.querySelectorAll('img[data-src]')
imgs.forEach((item, index) => {
if (item.dataset.src === '') return
// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
let rect = item.getBoundingClientRect()
if (rect.bottom >= 0 && rect.top < viewHeight) {
item.src = item.dataset.src
item.removeAttribute('data-src')//移除属性,下次不再遍历
}
})
}
lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
document.addEventListener("scroll",lazyload)
</script>
</body>
</html>
b.IntersectionObserver交叉观察器:
目标元素与父/视口产生交叉区
使用场景:懒加载图片、实现无限滚动、检测元素可见性等
常用函数
//创建观察器new IntersectionObserver(callback, options)
var io = new IntersectionObserver(callback, option)
// 绑定观察元素 observer.observe(dom)
io.observe(document.getElementById('example'))
// 停止观察
io.unobserve(element)
// 关闭观察器
io.disconnect()
使用:this.$nextTick(()=>{DOM更新后观察})
//列表发生变化时调用handleObserver()
handleObserver(){
if (!this.observer) {
this.observer = new IntersectionObserver((entries, self) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let img = entry.target
let src = img.dataset.src
if (src) {
img.src = src
img.removeAttribute('data-src')
}
// 解除观察
self.unobserve(entry.target)
}
})
})
}
// DOM 更新后执行
this.$nextTick(() => {
imgs.forEach((image) => {
let dom = document.getElementById(image.id);
if (dom) {
observer.observe(dom);
}
})
});
}
参数
callback 是可见性变化时的回调函数,option 是配置对象(可选)
callback触发时机:刚进入视口(开始可见),完全离开视口(开始不可见)
var observer = new IntersectionObserver(function(entries,observer/self){
//回调函数参数entries 被观察IntersectionObserverEntry对象数组;观察器
}, options)
IntersectionObserverEntry 对象的属性
常用:
- isIntersecting: 目标是否可见
- target:被观察的目标元素,是一个 DOM 节点对象
其他:
- time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
- rootBounds:根元素的矩形区域的信息,
getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回 null - boundingClientRect:目标元素的矩形区域的信息
- intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
- intersectionRatio:目标元素的可见比例,即
intersectionRect
占boundingClientRect
的比例,完全可见时为 1,完全不可见时小于等于 0
//默认值
const options = {
root: null, // 默认为整个文档视口;若设置祖先级对象,主要的是针对局部的滚动效果
rootMargin: '0px',//视口外延像素
threshold: 0 // 目标元素可见度的阈值,范围0(刚进入)到 1(完全可见)
};
const observer = new IntersectionObserver(callback, options);
B.框架插件
a.vue-lazyload
插件
// 安装
npm install vue-lazyload
// main.js 注册
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
// 配置项
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png', // 图片加载失败时的占位图
loading: 'dist/loading.gif', // 图片加载中时的占位图
attempt: 1
})
// 通过 v-lazy 指令使用
<ul>
<li v-for="img in list">
<img v-lazy="img.src" :key="img.src" >
</li>
</ul>
小图片转 base64 编码:无需请求
原理
图片的 二进制数据 的 字符串 表示形式,图片大小->原文件的 4/3
类似html中嵌入的文字,因此无需单独文件(无法缓存),从而无需请求
举例
小图片(小于 100KB),如小图标、图像按钮、装饰性背景图;
经常用到(如头像)
Data URL
格式
HTML
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA..." alt="Embedded Image">
CSS
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA...');
方式
a.FileReader
的 readAsDataURL()
getBase64(file: any) {
//转化base64
let _ = this;
return new Promise((resolve, reject) => {
let reader: any = new FileReader();
let base64 = '';
reader.readAsDataURL(file); //开始转
reader.onload = function() {
base64 = reader.result;
}; //转 失败
reader.onerror = function(error: any) {
reject(error);
}; //转 结束 resolve 出去
reader.onloadend = function() {
resolve(base64);
};
});
},
b.
canvas.toDataURL
c.url-loader
将图片转 base64
更多推荐
所有评论(0)