前端加快初始渲染的方式有哪些

552 阅读5分钟

start

2025年,回首过往,思绪如窗前的苍蝇,嗡嗡作响。此刻,我打开记忆的窗,倒一倒脑子,看看那些岁月里留下了什么。

减少DOM操作

使用文档片段 DocumentFragment

频繁的 DOM 操作会严重影响性能,尤其是当操作大量元素时。使用文档片段可以将多次 DOM 操作合并为一次,从而减少性能损耗。

const f = document.createDocumentFragment();
for (let i = 0; i < 10000; i++) {
	const d = document.createElement('div');
	d.className = `d_item d_${i + 1}`;
	d.textContent = `我是div,我的位置是:${i + 1}`;
	f.appendChild(d);
}
document.body.appendChild(f);

事件委托

将事件处理程序绑定到父元素上,而不是每个子元素上,可以减少事件处理程序的数量,提高性能。

// 生成大量 dom
const f = document.createDocumentFragment();
for (let i = 0; i < 10000; i++) {
	const d = document.createElement('div');
	d.className = `d_item d_${i + 1}`;
	d.id = `${i + 1}`;
	d.textContent = `我是div,我的位置是:${i + 1}`;
	f.appendChild(d);
}
document.body.appendChild(f);
// 在 body 上面进行委托
document.querySelector('body').addEventListener('click', function(e) {
	console.log('位置为:', e.target.id)
})

优化资源加载

懒加载

对于非关键资源(如图片、组件等),可以使用懒加载技术,仅在需要时才加载,从而减少初始加载时间。

滚动的时候,查看浏览器控制台「网络」栏,能观察到图片加载情况

浏览器提供的API IntersectionObserver

<style>
	img {
		height: 300px;
	}

	div {
		height: 320px;
		border: solid 1px red;
		background: pink;
	}
</style>
<div></div>
<div></div>
<div></div>
<div></div>
<img class="lazy_img" data-src="1.jpg" alt="懒加载图片1.jpg" />
<div></div>
<div></div>
<img class="lazy_img" data-src="2.jpg" alt="懒加载图片2.jpg" />
<div></div>
<div></div>
<img class="lazy_img" data-src="3.jpg" alt="懒加载图片3.jpg" />
<script>
	// 选择所有需要懒加载的图片
	const imgs = document.querySelectorAll('.lazy_img');
	// 创建一个 IntersectionObserver 实例
	const observer = new IntersectionObserver((entries, observer) => {
		entries.forEach(entry => {
			if (entry.isIntersecting) {
				const img = entry.target;
				img.src = img.dataset.src; // 将图片的真实地址赋值给src
				img.onload = () => img.classList.remove('lazy_img'); // 图片加载完成后移除lazy_img类
				observer.unobserve(img); // 停止观察该图片
			}
		});
	}, {
		rootMargin: '0px 0px 300px 0px' // 提前加载300px范围内的图片
	});
	// 开始观察每个懒加载图片
	imgs.forEach(image => observer.observe(image));
</script>

html属性

<style>
	img {
		height: 300px;
	}

	div {
		height: 320px;
		border: solid 1px red;
		background: pink;
	}
</style>
<div></div>
<div></div>
<div></div>
<div></div>
<img class="lazy_img" src="1.jpg" alt="懒加载图片1.jpg" loading="lazy" />
<div></div>
<div></div>
<img class="lazy_img" src="2.jpg" alt="懒加载图片2.jpg" loading="lazy" />
<div></div>
<div></div>
<img class="lazy_img" src="3.jpg" alt="懒加载图片3.jpg" loading="lazy" />

使用滚动事件scroll监听

<style>
	img {
		height: 300px;
	}

	div {
		height: 320px;
		border: solid 1px red;
		background: pink;
	}
</style>
<div></div>
<div></div>
<div></div>
<div></div>
<img class="lazy_img" data-src="1.jpg" alt="懒加载图片1.jpg" />
<div></div>
<div></div>
<img class="lazy_img" data-src="2.jpg" alt="懒加载图片2.jpg" />
<div></div>
<div></div>
<img class="lazy_img" data-src="3.jpg" alt="懒加载图片3.jpg" />
<div></div>
<script>
	window.addEventListener('scroll', () => {
		const imgs = document.querySelectorAll('.lazy_img');
		imgs.forEach(img => {
			if (img.getBoundingClientRect().top < window.innerHeight + 300) { // 预加载300px范围内的图片
				img.src = img.dataset.src;
				img.onload = () => img.classList.remove('lazy_img');
				img.onerror = () => img.classList.remove('lazy_img'); // 错误监听
			}
		});
	});
</script>

使用 CDN

将静态资源(如图片、JS、CSS)托管在 CDN 上,可以大幅提高资源的加载速度。

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

使用 WebP 格式图片

WebP 是一种现代的图片格式,提供了更好的压缩率和更小的文件大小,从而加快图片加载速度。

代码优化

代码分割与按需加载

通过代码分割和按需加载,可以减少初始加载时的资源请求,从而提高加载速度。

const routes = [{
	path: "/center",
	component: () => import("@/views/center/index.vue"),
	meta: {
		title: "账户中心",
	},
}, {
	path: "/review",
	component: () => import("@/views/review/index.vue"),
	meta: {
		title: "提现审核",
	},
}, {
	path: "/cash",
	component: () => import("@/views/cash/index.vue"),
	meta: {
		title: "审核提现",
	},
}]

使用 Web Workers

对于复杂的计算任务,可以使用 Web Workers 将其移到后台线程中执行,避免阻塞主线程。

<button>点击测试</button>
<script>
	document.querySelector('button').addEventListener('click', function() {
		const data = {
			value: 10
		}; // 示例数据
		// 主线程
		const worker = new Worker('test.js');
		worker.postMessage(data);
		worker.onmessage = function(event) {
			console.log('计算结果:', event.data);
		};
	})
</script>
self.onmessage = function(event) {
	const result = complexCalculation(event.data);
	self.postMessage(result);
};

function complexCalculation(data) {
	// 示例复杂计算:将传入的值乘以2
	return data.value * 2;
}

渲染优化

虚拟列表

对于长列表数据,可以使用虚拟列表技术,仅渲染视口内的元素,从而减少渲染压力。

<style>
	.viewport {
		border: solid 1px red;
	}

	.scroll_container {
		border: solid 1px purple;
	}

	.item {
		display: flex;
		align-items: center;
		justify-content: center;
		box-sizing: border-box;
		position: relative;
	}

	.item::before {
		content: '';
		position: absolute;
		left: 0;
		right: 0;
		bottom: 0;
		height: 1px;
		background: pink;
	}
</style>
<div class="viewport" style="height: 300px; overflow-y: auto;">
	<div class="scroll_container" style="height: 10000px;">
		<div class="visible_items" style="position: relative; top: 0;">
			<!-- 可见的数据项 -->
		</div>
	</div>
</div>
<script>
	const viewport = document.querySelector('.viewport');
	const scrollContainer = document.querySelector('.scroll_container');
	const visibleItemsContainer = document.querySelector('.visible_items');
	const itemHeight = 50; // 每条数据的高度
	const totalItems = 1000; // 数据总量
	const visibleItemCount = Math.ceil(viewport.clientHeight / itemHeight) + 1; // 可见数据数量
	// 设置滚动区域的总高度
	scrollContainer.style.height = `${totalItems * itemHeight}px`;

	function renderVisibleItems() {
		const scrollTop = viewport.scrollTop;
		const startIndex = Math.floor(scrollTop / itemHeight);
		// 检测最大值
		const endIndex = Math.min(startIndex + visibleItemCount, totalItems);
		// 清空当前可见的数据项
		visibleItemsContainer.innerHTML = '';
		// 渲染可见的数据项
		for (let i = startIndex; i < endIndex; i++) {
			const item = document.createElement('div');
			const num = i + 1;
			item.style.height = `${itemHeight}px`;
			item.textContent = `Item ${num}`;
			item.className = 'item';
			item.id = num;
			visibleItemsContainer.appendChild(item);
		}
		// 设置可见数据项的偏移量
		visibleItemsContainer.style.top = `${startIndex * itemHeight}px`;
	}
	// 监听滚动事件
	viewport.addEventListener('scroll', renderVisibleItems);
	// 初始渲染
	renderVisibleItems();
</script>

使用 requestAnimationFrame

在执行动画时,requestAnimationFrame 是比 setTimeout 更好的选择,因为它会在浏览器下一次重绘之前调用指定的回调函数。

<style>
	#move_box {
		width: 50px;
		height: 50px;
		background: red;
		position: relative;
		top: 0;
	}
</style>
<button>执行动画</button>
<div id="move_box"></div>
<script>
	let position = 0;
	let isAnimating = false; // 用于标记动画是否正在运行
	const oBox = document.querySelector('#move_box');
	document.querySelector('button').addEventListener('click', function() {
		if (!isAnimating) { // 如果动画未运行,则开始动画
			position = 0; // 重置位置
			isAnimating = true; // 标记动画正在运行
			requestAnimationFrame(animate); // 开始动画
		}
	})

	function animate() {
		position += 1; // 每帧移动1像素
		oBox.style.left = position + 'px'; // 更新方块位置
		if (position < 300) { // 限制移动距离
			requestAnimationFrame(animate); // 请求下一帧动画
		} else {
			isAnimating = false; // 动画结束,取消标记
		}
	}
</script>

减少重排和重绘

避免频繁修改样式

尽量将样式修改集中在一起,以减少浏览器的重绘和重排。

const oDiv = document.getoDivById("div");
oDiv.style.color = "red";
oDiv.style.fontSize = "18px";
oDiv.style.margin = "8px";

使用 transform 和 opacity

使用 transform 和 opacity 进行动画操作,这些属性不会触发重排和重绘,只会触发最终的合成阶段,性能更好。

<style>
	html,
	body {
		height: 100%;
	}

	body {
		margin: 0;
		display: flex;
		justify-content: center;
		align-items: center;
	}

	div {
		width: 100px;
		height: 100px;
		background: red;
		transition: transform .3s, opacity 3s;
	}

	div:hover {
		transform: scale(2);
		opacity: .8;
	}
</style>
<div></div>

使用服务端渲染(SSR)

对于 SEO 和首屏加载速度要求较高的应用,可以使用服务端渲染(SSR)。SSR 的核心思想是将页面的初始 HTML 在服务器端生成,然后直接发送给客户端,客户端接收到完整的 HTML 后直接渲染页面,从而大大减少了首屏渲染时间。

SSR 最快的理解方式就是,和曾经的 JSP、html 嵌入 PHP、ASP 都差不多

SSR 的优势

  1. 首屏加载速度快:服务器直接返回完整的 HTML,客户端无需等待 JavaScript 加载和执行即可渲染页面。
  2. SEO 友好:搜索引擎可以直接抓取服务器返回的 HTML 内容,无需等待 JavaScript 执行,从而提高 SEO 效果。
  3. 减少客户端负担:服务器端完成 HTML 的生成,客户端只需进行简单的渲染,减轻了客户端的计算压力。

SSR 的实现

这里为了简化,可以直接使用 Nuxt.js 官方提供的简单例子,下面附赠创建项目和运行命令行

npx create-nuxt-app nuxt-test
cd nuxt-test
npm run build
npm run start

SSR 的注意事项

  1. 服务器性能:SSR 会增加服务器的负担,因为服务器需要动态生成 HTML。因此,需要确保服务器有足够的资源来处理请求。
  2. 数据预取:在服务器端渲染页面时,可以使用 asyncData 或 fetch 方法预取数据,确保页面加载时数据已经准备好。
  3. 缓存策略:对于静态内容,可以使用缓存策略来减少服务器的重复计算,提高性能。

使用静态站点生成(SSG)

上面 SSR 运行的时候 SSR 和 SSG 同一个选项,这里不重复了,说一下两者的区别,方便快速理解和记忆

SSR 和 SSG 对比

特点SSR(服务器端渲染)SSG(静态站点生成)
生成时机每次请求时动态生成构建时静态生成
性能需要服务器处理,性能稍低直接返回静态文件,性能更高
适用场景动态内容频繁更新静态内容不经常更新
SEOSEO友好SEO友好
首屏加载首屏加载快首屏加载快
资源消耗需要服务器资源不需要服务器资源,适合CDN部署
更新频率适合高频率更新适合低频率更新
  • SSR:适合需要动态生成内容的场景,如新闻网站、电商平台等。每次请求都会动态生成HTML内容,适合内容频繁更新的场景。
  • SSG:适合静态内容不经常更新的场景,如博客、文档等。在构建时生成静态HTML文件,适合内容更新不频繁的场景,性能更高,适合大规模部署。

SSG 的注意事项

  1. 数据更新:如果数据经常更新,需要考虑如何重新生成静态页面。Next.js 提供了 revalidate 选项,可以在指定的时间间隔内重新生成页面。
  2. 构建时间:SSG 的构建时间可能会比较长,尤其是在数据量较大的情况下。需要合理优化构建过程。

使用缓存策略

缓存是提高性能的关键手段之一。通过合理配置缓存策略,可以减少重复请求,加快资源加载速度。

通过设置 HTTP 缓存头(如 Cache-Control),可以让浏览器缓存静态资源。

在服务器端配置缓存头:

# 设置 static 路径
location /static/ {
	# 设置缓存时间为1年
    expires 1y;
    add_header Cache-Control "public";
}

end

恳请诸位同仁、挚友、兄弟姐妹不吝赐教,多多指点!