前端性能优化之首屏加载【懒加载,监听元素视口可见,骨架屏优化白屏时长,长列表虚拟滚动,压缩图片,Tree shaking,base64】

发布时间:2026/7/21 20:15:09
前端性能优化之首屏加载【懒加载,监听元素视口可见,骨架屏优化白屏时长,长列表虚拟滚动,压缩图片,Tree shaking,base64】 目录路由懒加载SPA 一个路由对应一个页面把所有页面打包成一个文件懒加载前提ES6动态地加载模块——import()webpackChunkName分离到单独的 chunk组件懒加载webpackChunkName: dialogInfo适用场景体积大非首屏复用性高Tree shaking消除无用的 JS 代码减少代码体积原理ES6模块静态分析只适用于函数式编程骨架屏优化白屏时长虚拟列表大量数据长列表虚拟滚动只渲染可视区非可见区域的不渲染虚拟滚动插件Web Worker 优化长任务适用当任务的运算时长 - 通信时长 50msrequestAnimationFrame 制作动画刷新频率与显示器的频率保持一致优先级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格式HTMLCSS方式a.FileReader 的 readAsDataURL()b.canvas.toDataURLc.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: dialogInfohome 页面 和 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-virtualizedWeb Worker 优化长任务由于浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系当页面中有很多长任务时会造成页面 UI 阻塞出现界面卡顿、掉帧等情况查看页面的长任务打开控制台选择 Performance 工具点击 Start 按钮展开 Main 选项会发现有很多红色的三角这些就属于长任务长任务执行时间超过50ms的任务适用当任务的运算时长 - 通信时长 50msTime 是这个资源的通信时长也叫加载时长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 元素中存储额外的信息而不会自动请求资源在图片出现在屏幕可视区域的时候再将>img src alt>动态裁剪在图片url地址上动态添加参数比如阿里云或七牛云都提供了图片的动态裁剪功懒加载/视口可见元素(Load On Demand)延迟加载、按需加载可视化区域之外的图片不会进行加载在滚动屏幕时才加载。这样使得网页的加载速度更快减少了服务器的负载。懒加载适用于图片较多页面列表较长(长列表)的场景中。A.浏览器原生监听器节流和防抖能减少执行次数但不能消除scroll频繁触发的本质IntersectionObserver在浏览器的层面处理可见性检测能够批量处理多个观察者的状态变化减少计算和触发的频率IntersectionObserver2016 年( Chrome 51 和 Firefox 55因此scroll更兼容a.scroll 视口顶部距离使用场景检测滚动位置存储初始边界值滚动会更改相应距离位置scroll回调放在requestAnimationFrame中以在浏览器下次重绘前执行!DOCTYPE html html langen head meta charsetUTF-8 meta http-equivX-UA-Compatible contentIEedge meta nameviewport contentwidthdevice-width, initial-scale1.0 titleDocument/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 classscrollBox h1孙悟空/h1 h1孙悟空/h1 h1孙悟空/h1 h1孙悟空/h1 h1孙悟空/h1 h1孙悟空/h1 h1孙悟空/h1 h1孙悟空/h1 h1孙悟空/h1 h1孙悟空/h1 div classtarget/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 属性设为默认图片图片的真实路径则设置在data-src属性中绑定 window 的scroll事件对其进行事件监听。//节流 window.addEventListener(scroll, throttle(lazyload, 200))在scroll事件的回调中判断我们的懒加载的图片是否进入可视区域,如果图片在可视区内将图片的 src 属性设置为data-src的值html langen head meta charsetUTF-8 titleLazyload/title style .image-item { display: block; margin-bottom: 50px; height: 200px;//一定记得设置图片高度 } /style /head body img src./img/default.png>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(entriesobserver/self){ //回调函数参数entries 被观察IntersectionObserverEntry对象数组观察器 }, options)IntersectionObserverEntry 对象的属性常用isIntersecting: 目标是否可见target被观察的目标元素是一个 DOM 节点对象其他time可见性发生变化的时间是一个高精度时间戳单位为毫秒rootBounds根元素的矩形区域的信息getBoundingClientRect()方法的返回值如果没有根元素即直接相对于视口滚动则返回 nullboundingClientRect目标元素的矩形区域的信息intersectionRect目标元素与视口或根元素的交叉区域的信息intersectionRatio目标元素的可见比例即intersectionRect占boundingClientRect的比例完全可见时为 1完全不可见时小于等于 0options配置对象//默认值 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-forimg in list img v-lazyimg.src :keyimg.src /li /ul前端性能优化之图片懒加载 - 掘金预加载某图片加载时机方法加载时机推荐度CSS 隐藏但渲染页面加载时★★★☆☆link relpreload文档解析早期★★★★★JSnew Image()JS执行时加载好再显示★★★★☆link relpreload!-- 在HTML head中 -- link relpreload hrefimage.jpg asimageJSnew Image()加载完再显示div v-showimageLoaded classglobal-modal :classmodalClass :stylemodalStyle data() { return { imageLoaded: false } } mounted() { if (this.theme img) { const img new Image(); img.onload () { this.imageLoaded true; }; img.src require(/assets/images/v3/certificate_bg.png); } else { this.imageLoaded true; } }CSS 隐藏但渲染/* 创建一个渲染但不可见的元素 */ .preload-images { position: absolute; width: 0; height: 0; overflow: hidden; z-index: -1; opacity: 0.001; /* 几乎透明但确保渲染 */ /* 关键不要用 display: none */ background: url(/assets/images/bg1.jpg), url(/assets/images/bg2.jpg), url(/assets/images/bg3.jpg); }小图片转 base64 编码无需请求原理图片的 二进制数据 的 字符串 表示形式图片大小-原文件的 4/3类似html中嵌入的文字因此无需单独文件无法缓存从而无需请求举例小图片小于 100KB如小图标、图像按钮、装饰性背景图经常用到如头像Data URL格式HTMLimg srcdata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA... altEmbedded ImageCSSbackground-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.toDataURLc.url-loader将图片转 base64参考链接前端性能优化——首页资源压缩63%、白屏时间缩短86% - 掘金