目录

路由懒加载

SPA :一个路由对应一个页面

把所有页面打包成一个文件

懒加载前提:ES6动态地加载模块——import()

webpackChunkName:分离到单独的 chunk

组件懒加载

webpackChunkName: "dialogInfo"

适用场景

体积大

非首屏

复用性高

Tree shaking:消除无用的 JS 代码,减少代码体积

原理:ES6模块静态分析

只适用于函数式编程

骨架屏优化白屏时长

虚拟列表:大量数据

长列表虚拟滚动:只渲染可视区,非可见区域的不渲染

虚拟滚动插件

Web Worker 优化长任务

适用:当任务的运算时长 - 通信时长 > 50ms

requestAnimationFrame 制作动画:刷新频率与显示器的频率保持一致

优先级

setTimeout/setInterval 属于 JS引擎

requestAnimationFrame 属于 GUI引擎

JS引擎与GUI引擎是互斥的: GUI 引擎在渲染时会阻塞 JS 引擎的计算

时间

requestAnimationFrame 刷新频率是固定且准确的

setTimeout/setInterval 是宏任务

性能

setTimeout/setInterval 定时器仍会在后台执行动画任务,

requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停

列表(以图片为例)

src属性

src :自动发送请求、下载

data-src :暂存 src 的值

动态裁剪:在图片url地址上动态添加参数

懒加载/视口可见元素

A.浏览器原生监听器

a.scroll +视口顶部距离

b.IntersectionObserver交叉观察器:目标元素与父/视口产生交叉区

常用函数

使用:this.$nextTick(()=>{DOM更新后观察})

参数

B.框架插件

a.vue-lazyload 插件

小图片转 base64 编码:无需请求

原理

图片的 二进制数据 的 字符串 表示形式,图片大小->原文件的 4/3

类似html中嵌入的文字,因此无需单独文件(无法缓存),从而无需请求

举例

小图片(小于 100KB),如小图标、图像按钮、装饰性背景图;

经常用到(如头像)

Data URL

格式

HTML

CSS 

方式

a.FileReader 的 readAsDataURL()

b.canvas.toDataURL

c.url-loader 将图片转 base64


懒/动态加载:按需/运动加载

路由懒加载

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在浏览器的层面处理可见性检测,能够批量处理多个观察者的状态变化,减少计算和触发的频率

IntersectionObserver2016 年( 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>

图片的 src 属性设为默认图片

  1. 图片的真实路径则设置在data-src属性中,
  2. 绑定 window 的 scroll 事件,对其进行事件监听。
  3. //节流
    window.addEventListener('scroll', throttle(lazyload, 200))
  4. 在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,
  5. 如果图片在可视区内将图片的 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:目标元素的可见比例,即 intersectionRectboundingClientRect 的比例,完全可见时为 1,完全不可见时小于等于 0

options配置对象

//默认值
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.FileReaderreadAsDataURL()
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

参考链接:前端性能优化——首页资源压缩63%、白屏时间缩短86% - 掘金

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐