如何开发高性能的前端


个性化网站就像在走钢丝。我们既想要闪电般的性能,也想要为最终用户提供深度的个性化体验,同时又要便于管理。听起来简单,对吧?其实不然。

在这篇文章中,我们将探讨几种不同的个性化方法,从服务器端渲染到边缘裁剪。我们会分析各自的优缺点,以及它们在性能、灵活性和易用性之间的平衡。

我们的目标

在为现代前端应用实现个性化时,我们追求以下四点:

    1. 最佳性能:页面加载要快。非常快。
    1. 细粒度且灵活的个性化:我们需要深度且灵活的个性化,而不仅仅是替换一行文本。
    1. 简易创建和管理:我们不希望为了更改一张图片而与复杂的系统纠缠不清。
    1. 低成本:我们希望保持基础设施和运营成本较低。

此外,我们希望支持任何现代前端框架(如 React、Next.js、Vue、Svelte、Remix、Qwik 等)以及几乎所有的托管提供商和 CDN。

听起来足够简单吧?实际上没那么容易。要同时实现这四个目标,就像试图把果冻钉在墙上一样,既棘手又混乱,而且最终可能会弄得满手黏糊糊的。不过别担心,我们将探索几种方法,帮助你找到最适合自己需求的平衡点。

方法 1:每个请求都进行服务器端渲染

这就像每次你饿了都有一位私人厨师为你现做一餐。听起来不错,但实际上并不总是可行。

在代码中,比如在 Next.js 中,它可能是这样的:

1
2
3
4
5
6
7
8
9
export async function getServerSideProps({ req }) {
const user = await getUserData(req.cookies.userId);
const content = await getContent({
model: 'page',
userAttributes: { audiences: user.interests }
});

return { props: { content } };
}

每个请求都进行服务器端渲染从某些方面来说是最直接的个性化方法。每当用户请求页面时,服务器就会激活,获取所有必要的数据,并提供一个完全定制化的页面。这为个性化提供了极大的灵活性。

但问题是:它很慢。每次页面访问都需要服务器来回往返进行处理,然后再向用户发送内容。如果用户在澳大利亚而服务器在美国,那么数据的传输距离相当远。

此外,如果你的网站变得流行起来,你的服务器可能会像程序员解释项目延误原因时一样满头大汗。而且这种方式的成本也不低。

优点

  • 高度灵活的个性化
  • 服务器完全控制输出

缺点

  • 性能可能受影响
  • 扩展性成本较高

评分

  • 性能:⭐⭐(2/5)
  • 灵活性:⭐⭐⭐⭐⭐(5/5)
  • 创建/管理的简易性:⭐⭐⭐⭐(4/5)
  • 成本:⭐(1/5)

方法 2:边缘缓存 + 客户端注入

这就像预先煮好的饭送到你家门口,而你自己再加上特制的酱料。

在这种情况下,我们会缓存页面的核心部分(无需个性化的部分),并在客户端填充个性化内容,比如在加载完成之前显示一个占位符。

我们可以将个性化部分移至客户端,例如在 React 中:

1
2
3
4
5
6
7
8
9
10
const [contentPromise] = useState(async function() {
const user = await getUser();
// 获取该用户的个性化内容
const content = await getContent({
model: 'hero',
userAttributes: { audiences: user.audiences }
});
return content;
});
const content = use(contentPromise);

边缘缓存和客户端注入为你提供了静态内容的加载速度和个性化的灵活性。你可以通过 CDN 提供一个缓存的静态页面,这样页面加载速度非常快(这是页面加载的最快方式——CDN 直接到用户)。然后,JavaScript 开始运行,获取个性化内容并注入到页面中。

这种方法很受欢迎,因为它相对容易实现,适用于任何前端和后端技术栈,且成本较低。但这并不是十全十美的。页面加载和个性化内容显示之间常常存在明显的延迟,这会导致不连贯的用户体验。

另一个问题是 SEO。搜索引擎可能看不到你的个性化内容,因为它们不会执行 JavaScript。而且尽管页面初始加载很快,但页面加载后你仍然需要额外的 API 调用,这可能会降低整体体验。

优点

  • 初始页面加载速度快
  • 适用于任何技术栈

缺点

  • 页面加载与个性化内容显示之间存在延迟
  • SEO 问题

评分

  • 性能:⭐⭐⭐(3/5)
  • 灵活性:⭐⭐⭐⭐(4/5)
  • 创建/管理的简易性:⭐⭐⭐(4/5)
  • 成本:⭐⭐⭐⭐(4/5)

    方法 3:边缘渲染

    这就像拥有一位私人厨师,但他们就在你家外面的餐车里。

比如,使用 Remix 的 loader 并在 Cloudflare 的边缘运行,它的代码可能是这样的:

1
2
3
4
5
6
7
8
async function loader(request) {
const user = await getUser(request);
const content = await getContent({
model: 'page',
userAttributes: { audiences: user.interests }
});
return content;
}

边缘渲染将计算过程靠近用户,减少延迟并提高性能。与传统服务器端渲染相比,用户的请求不需要传输到远处的中心服务器,而是只需到达最近的边缘节点。这种方法与按请求进行的服务器端渲染类似,但性能有所提升。

然而,边缘渲染并非一帆风顺。最大的问题之一是数据的可用性。并非所有数据都能在边缘节点轻松获取,可能需要跨国的数据往返传输,才能完全响应。此外,边缘计算环境往往有一些限制,可能使开发变得更复杂。

尽管有这些挑战,边缘渲染在许多情况下仍然非常强大。就像餐车里的高级厨师一样——在需要时非常有用,但如果只是为了提供热狗,可能就显得有点过于奢华了。

优点

  • 比传统服务器渲染快
  • 仍能提供高度个性化的内容

缺点

  • 并非所有数据都能在边缘节点获取
  • 流式处理等场景可能变得复杂

评分

  • 性能:⭐⭐⭐(3/5)
  • 灵活性:⭐⭐⭐⭐(4/5)
  • 创建/管理的简易性:⭐⭐⭐(3/5)
  • 成本:⭐⭐⭐(3/5)

方法 4:边缘增强

这就像点了一份标准餐,但送餐员在递给你之前为你加上了个性化的装饰。

在这种情况下,我们会先返回缓存的内容,然后根据需要在边缘节点动态替换部分内容。

1
2
3
4
5
6
7
8
async function handleRequest(request) {
const [html, user] = await Promise.all([
getHtml(request), getUser(request)
]);

// 用个性化信息替换特殊字符串
return html.replace('%%USER_NAME%%', user.name);
}

边缘增强是完全静态内容与为每个请求生成个性化页面之间的中间方案。你可以从预生成并缓存的页面开始,然后根据用户数据在发送给浏览器之前做出一些小的、有针对性的修改。

这种方法的优势在于,它结合了缓存内容的性能优势,同时仍然允许添加个性化元素。它特别适用于如注入用户名、根据用户位置替换图片等非常小范围的操作。

然而,需要注意的是,边缘增强并不能替代完全的服务器端渲染。你能做的个性化修改范围非常有限。由于你是在处理现有的 HTML 结构,想要对布局或内容结构进行大幅修改可能会变得很困难。

优点

  • 响应时间快
  • 允许一定程度的个性化

缺点

  • 个性化的范围有限
  • 对复杂的更改来说实现较为困难

评分

  • 性能:⭐⭐⭐⭐⭐(5/5)
  • 灵活性:⭐⭐⭐(3/5)
  • 创建/管理的简易性:⭐⭐(2/5)
  • 成本:⭐⭐⭐⭐(4/5)

    方法 5:边缘分段

    这就像准备几种不同的预制餐食,并根据你对用餐者的了解选择提供哪一种。

在这种方法中,我们将根据用户的属性将其重定向到有限的分段集合。

1
2
3
4
5
6
7
8
9
async function handleRequest(request) {
if (!request.url.searchParams.has('segment')) {
const userSegment = getUserSegment(request);
const newURL = new URL(request.url);
newURL.searchParams.set('segment', userSegment);
redirect(newURL);
}
// 为该分段提供内容,例如在 ?segment=...
}

边缘分段的工作原理是确定用户属于哪个分段,然后为他们提供一个预生成的页面版本,这个页面是针对该分段量身定制的。这些页面可以是静态生成的,或者只是简单的服务器端渲染页面,使用陈旧-再验证缓存策略(例如:Cache-Control: stale-while-revalidate=...)。

这种方法比边缘增强提供了更显著的个性化,同时仍然保持出色的性能。

不过,你的分段数量是有限的。你不能有无限数量的变体,否则就会失去缓存的好处。这意味着你需要仔细选择分段,以确保涵盖最重要的用例,而不会创建过多的变体。

尽管有这些挑战,边缘分段对于许多网站来说仍然是一个强大的方法。它特别适用于用户群体明确、需求不同的场景(例如,一个电子商务网站,已知的男装购物者、女装购物者或未知/匿名用户)。

这有点像经营一家餐厅,你为不同类型的用餐者设计了几种固定菜单。

优点

  • 响应时间快
  • 允许更显著的个性化

缺点

  • 限于固定数量的分段
  • 需要仔细规划分段

评分

  • 性能:⭐⭐⭐⭐⭐(5/5)
  • 灵活性:⭐⭐⭐(3/5)
  • 创建/管理的简易性:⭐⭐⭐(3/5)
  • 成本:⭐⭐⭐⭐(4/5)

方法 6:边缘修剪个性化

这就像给用餐者提供一份包含所有选项的菜单,但有一个了解他们偏好的服务员,指出他们最可能喜欢的菜品。

边缘修剪个性化提供一个包含所有内容变体的单一 HTML 文件,该文件可以在边缘进行积极缓存。然后,在边缘处理此 HTML,以仅显示特定用户的相关内容,然后再将其发送到浏览器。

1
2
3
4
5
6
7
async function handleRequest(request) {
const html = await getHtml(request);
const smallHtml = trimHtml(html, {
userAttributes: { audiences: user.audiences },
});
return html;
}

这种方法允许非常细致的个性化,对于 SEO 也非常友好,因为所有内容都在初始 HTML 中。这种方法甚至可以在没有边缘工作者的情况下工作。一些平台(如 Builder.io)使用内联脚本作为后备,根据用户的最佳匹配在客户端即时替换内容,同时页面正在渲染。

我认为这种方法非常强大。它与可视化编辑工具很好地配合,提供了很大的灵活性。就像经营一家餐厅,菜单上包含所有可能的菜品,但每位用餐者只看到与他们相关的菜品。

这种方法的一个关键优势是,即使没有边缘工作者进行 HTML 修剪,也能正常工作。默认情况下,HTML 是保存的,并带有内联脚本,这些脚本在客户端同步执行个性化,例如:

1
2
3
4
5
6
7
8
<div class="hero">
<h1 class="title">Hello there</h1>
<script>
if (userAttributes.segments.includes('returnVisitor')) {
document.currentScript.previousElementSibling.textContent = 'Welcome back!';
}
</script>
</div>

考虑到这一点,不需要 CDN,且由于 gzip 的特性,你可以在增大负载的情况下(例如,增加 5-10%)拥有大量的个性化内容。但如果使用 CDN 进行修剪,假设你在 returnVisitor 用户分段中,你得到的内容将是:

1
2
3
<div class="hero">
<h1 class="title">Welcome back!</h1>
</div>

这种方法的主要缺点是需要特殊的工具,可以跨现代框架生成这种代码(需考虑正确的水合作用等)。像 Builder.io 这样的平台支持这一点,并提供可视化编辑器,可以用自然语言进行编辑。

使用 Builder.io 用自然语言个性化页面某些部分的示例

然而,自行实现这一点可能会有点复杂,可能还是使用其他选项更好。

优点

  • 最优缓存
  • 允许高度细致的个性化
  • 边缘处灵活的个性化
  • SEO 友好

缺点

  • 需要特殊工具,且自行实现并非最简单

评分

  • 性能:⭐⭐⭐⭐⭐(5/5)
  • 灵活性:⭐⭐⭐⭐⭐(5/5)
  • 创建/管理的简易性:⭐⭐⭐⭐⭐(5/5)
  • 成本:⭐⭐⭐⭐(4/5)

    综合考虑

那么,哪种方法是最佳的?和技术中的大多数事情一样,这取决于你的具体用例。但这是我一般的建议:

    1. 从边缘缓存开始。这是良好性能的基础。
    1. 对于页中内容或非关键个性化,考虑客户端抓取,尤其是需要一对一个性化的内容,比如每个用户的推荐产品。
    1. 如果你有明确的用户分段且不需要超级细致的个性化,可以考虑边缘分段。
    1. 若想要一种最佳的折中方法,可以考虑像 Builder.io 这样的平台,它为开发者和终端用户提供工具,有效地实现边缘修剪个性化,以及其他所有选项。

请记住,目标是在不牺牲性能、灵活性、管理便利性或成本的情况下提供个性化体验。这是一种平衡,但有了这些工具,你就能够应对。

以下是每种方法的快速总结:

方法 性能 灵活性 易用性 成本
服务器端渲染 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
边缘缓存 + 客户端 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
边缘渲染 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐
边缘增强 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
边缘分段 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
边缘修剪个性化 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐

正如你所看到的,边缘修剪个性化在所有四个标准上提供了良好的平衡,这也是为什么它是我个人的最爱。
然而,最适合你的方法将取决于你的具体需求、资源和限制。

总结

个性化是一个强大的工具,但需要谨慎使用。通过理解这些不同的方法,你可以为你的具体需求选择合适的策略。

请记住:

  • 性能是关键。不要为个性化而牺牲性能。
  • 缓存是你的朋友。明智地使用它。
  • 保持灵活。网站的不同部分可能从不同的方法中受益。
  • 从小处入手,并逐步迭代。
  • 始终考虑你的用户。
  • 考虑你所选择的方法的成本影响。

网页个性化的世界正在不断发展。新的技术和方法不断涌现,推动着可能性的边界。保持好奇心,继续学习,不要害怕尝试。

请记住,个性化的目标不仅仅是向不同用户展示不同的内容,而是创造能引起共鸣的体验,让人感觉量身定制且相关。当个性化做到位时,可以让你的用户感受到被理解和重视,这真的很酷。