Syntax - Tasty Web Development Treats - 708:我们如何让 Syntax.fm 更快 封面

708:我们如何让 Syntax.fm 更快

708: How We Made Syntax.fm Faster

本集简介

在本期《Syntax》节目中,Wes 和 Scott 讨论了他们如何提升 Syntax.fm 网站的性能,如何最初发现网站速度缓慢,以及为改善网站速度所做出的缓存和加载字幕的各类调整。 节目笔记 00:25 欢迎 01:32 添加数据库需要查询 03:32 我们如何知道网站很慢? 04:25 Syntax 由 Sentry 赞助 07:45 改变字幕加载方式 13:41 缓存 21:16 缓存头 在社交媒体上联系我们! Syntax:X Instagram Tiktok LinkedIn Threads Wes:X Instagram Tiktok LinkedIn Threads Scott:X Instagram Tiktok LinkedIn Threads

双语字幕

仅展示文本字幕,不包含中文音频;想边听边看,请使用 Bayt 播客 App。

Speaker 0

星期一。

Monday.

Speaker 0

星期一。

Monday.

Speaker 0

星期一。

Monday.

Speaker 0

打开吧,开发者粉丝们。

Open wide dev fans.

Speaker 0

准备好大快朵颐吧:JavaScript、CSS、Node 模块,还有烧烤小贴士。

Get ready to stuff your face with JavaScript, CSS, node modules, barbecue tips.

Speaker 0

掌握工作流程、霹雳舞、软技能和网页开发。

Get workflows, breakdancing, soft skills, web development.

Speaker 0

最匆忙、最疯狂、最美味的网页开发盛宴即将登场。

The hastiest, the craziest, the tastiest web development treats coming in hot.

Speaker 0

这里是 Wes、Barracuda、老板和 Scott,El Toroloco Tolinsky。

Here is Wes, Barracuda, boss, and Scott, El Toroloco Tolinsky.

Speaker 1

欢迎来到Syntax。

Welcome to Syntax.

Speaker 1

这是第708期,我们感觉很棒。

It's episode number seven zero eight, and we're feeling great.

Speaker 1

我们将要讨论性能问题。

We're gonna be talking about performance here.

Speaker 1

我们上线了全新的Syntax网站。

We pushed, you know, a new Syntax website.

Speaker 1

我们为它投入了大量的工作。

We did a ton of work into it.

Speaker 1

你知道吗?

And you know what?

Speaker 1

它还可以更快。

It could always be faster.

Speaker 1

所以你知道我们做了什么吗?

So you know what we did?

Speaker 1

我们亲自上手做了优化。

We we got our hands dirty.

Speaker 1

我们查看了各种指标。

We looked at metrics.

Speaker 1

我们分析了时间线、火焰图以及所有相关数据,最终让网站更快了。

We looked at timelines, flame graphs, and all that stuff, and we made it faster.

Speaker 1

我们会向你们详细介绍我们为提升性能所采用的所有技巧和方法、使用的工具,以及过程中学到的经验。

We're gonna be telling you all about the tips and techniques that we implemented to make this thing faster, what tools we used, and what we learned along the way.

Speaker 1

我叫斯科特·塔林斯基。

My name is Scott Talinski.

Speaker 1

我是一名开发者。

I'm a developer.

Speaker 1

我来自丹佛。

I'm from Denver.

Speaker 1

和往常一样,和我一起的是韦斯·博斯。

With me as always is Wes Boss.

Speaker 1

嘿,老兄,最近怎么样?

What's up my man?

Speaker 2

嗨。

Hey.

Speaker 2

我非常期待聊这个话题,因为新网站上有几个地方有点卡顿,你知道的,原因各不相同。

I am excited to talk about this because there was several spots in the the new website that were a little sluggish, you know, and for different reasons.

Speaker 2

所以我觉得这集挺有意思的,我们可以聊聊:这些就是我们在性能各方面遇到的问题,我们是怎么找出拖慢速度的原因的,以及我们最终是怎么让它变快的。

So it's I think it's kind of an interesting episode to sort of say, all right, these are the problems that we hit on the different aspects of performance, and and here's how we figured out what was causing it to be slow, and here's how we actually made it faster.

Speaker 1

完全没错。

Totally.

Speaker 1

而且我觉得也很重要的是要说明,嘿,我们上线了,从一个相对简单的网站开始。

And I think it's also important to note that, like, Hey, we launched and we went from a site that was relatively simple.

Speaker 1

所有内容都是动态生成的。

Everything was really generated.

Speaker 1

那是一个静态网站。

It was a static site.

Speaker 1

当时,这些展示网站都是在编译时生成为HTML的,这对于内容不太频繁变动的网站来说非常理想。

It was, it, it was basically any of these show websites were generated into HTML at compile time, which is really great with a site that doesn't change dynamically that much.

Speaker 1

但当我们转向这个新网站时,一个重大变化是我们有了数据库。

But one of the big changes that we had when moving to this new site was that, Hey, we got a database now.

Speaker 1

一旦引入了数据库,你就同时引入了服务器端的处理流程之类的东西。

And the moment you bring in a database, you bring in server side processes and things like that.

Speaker 1

你突然间就得担心一些新问题了。

You suddenly have some things you have to worry about.

Speaker 1

你得面对一些更容易变慢的环节,因为数据不再是以单个文件的形式缓存、随时可加载,而是必须执行数据库查询。

You have areas where it's much easier to be slow because instead of having that data be cached as single files that can be loaded and ready to go, gotta do a database query.

Speaker 1

你得检查一些东西,得完成各种额外的步骤,这些都可能导致速度变慢。

You gotta check You some things gotta have all sorts of additional steps that can bring on slowness.

Speaker 1

而这些问题的根源大多就在于此。

And that's really what the problems mostly were.

Speaker 1

我认为我们遇到的所有问题,几乎都是通过服务器端的修复解决的,而不是客户端的修复。

I I would say just about all of our problems were solved with server side fixes rather than client side fixes.

Speaker 1

是的。

Yeah.

Speaker 2

不过,我得纠正你一下。

And I'll I'll correct you there, though.

Speaker 2

旧网站并不是预生成的。

The old site wasn't pregenerated.

Speaker 2

像,不是

Like, wasn't

Speaker 1

它不是。

It wasn't.

Speaker 2

HTML。

HTML.

Speaker 2

哦,是吗?

Oh, is it?

Speaker 2

不是。

No.

Speaker 2

它是按需生成的。

It it was generated on demand.

Speaker 2

之所以按需生成,是因为我们需要在新剧集一发布就立即推送,而不想在每周五早上9点重新生成所有内容。

And the only reason it was generated on demand is because we needed to be able to push out new shows as soon as they were released, and we didn't wanna have to regenerate every single at like 9AM on

Speaker 1

哦,对。

Oh, right.

Speaker 2

周五早上。

Friday morning.

Speaker 2

所以我们采用了‘过期时重新验证’的策略:用户访问网站时,系统会先返回缓存的版本,同时在后台为下一位用户生成新版本。

So we we did the stale while revalidate where you visited the website and then it gave you the cached version and then it generated a new version in the background for the next person.

Speaker 1

是的。

Yes.

Speaker 1

这是在 Next.js 中实现的。

And that was in Next.

Speaker 1

Js。

Js.

Speaker 1

我之所以搞错了,其中一个原因是韦斯主要负责搭建了那个最初的网站。

The one of the reasons why I got that wrong is because Wes Wes primarily knocked out the the first website.

Speaker 1

所以我最初并没有参与太多建设工作。

So I didn't have a whole lot of hand in building it initially.

Speaker 1

但这个网站,同样,它没有数据库。

But this website, again, that one didn't have a database.

Speaker 1

我认为这是最关键的一点。

I think it was the big thing.

Speaker 1

而数据库方面——

And the database aspect-

Speaker 2

只是用 Markdown 文件。

Just markdown files.

Speaker 1

是的。

Yeah.

Speaker 1

这其实是整个系统中最繁重的部分之一。

Is really one of the heavier loads of it all.

Speaker 1

所以首先,我们是怎么知道它很慢的?

So first and foremost, how did we know it was slow?

Speaker 1

我们去了哪里?

Where did we go?

Speaker 1

我们做了什么来诊断它慢的问题?

What did we do to diagnose this for being slow and diagnose this for being slow?

Speaker 1

对我来说,你有一部分是靠肉眼观察。

For me, you you had some of it was the eye test.

Speaker 1

你使用这个网站时,会觉得它没有应有的响应速度。

You use the site and you say, this thing doesn't feel as fast as it should feel.

Speaker 1

我的网络速度非常快。

And I have really fast internet.

Speaker 1

我认为这里肯定存在问题,而且这些情况可以通过各种数据来验证。

I would imagine that there's some issues here and that can be backed up by all kinds of data.

Speaker 1

当然,你可以打开开发者工具。

I mean, granted you could load up your dev tools.

Speaker 1

你可以把你的网络速度设置为3G,然后在网络标签页中加载,这样就能进行测量。

You could, you know, set your Internet speed to be three g and your your network tab, and you can load it up that way and measure it.

Speaker 1

你也可以用Lighthouse或者其他各种工具来运行测试。

You can run it through lighthouse or all kinds of things.

Speaker 1

但因为我们现在是由Sentry赞助的,所以我们使用了Sentry,我不希望整个这一集变成Sentry的广告,但我们确实大量使用了Sentry的性能工具来解决我们的问题。

But what we did is since, you know, we're presented by Sentry here, we use Sentry and I don't want this whole episode to turn into a Sentry ad, but we did use Sentry's performance tools in absurd amount to solve our issues.

Speaker 1

而且我认为有必要强调这一点,因为我们加载了Sentry的性能工具,它会展示每个路由的Web核心指标。

And and I think it's important to highlight that because we loaded up the the Sentry performance tools and they're showing you web vitals across every route.

Speaker 1

在这个页面上首先显示的信息就是:这是你网站中最慢的路由,不一定是页面最慢,而是路由最慢。

And one of the things that shows you first and foremost on that page is that here is the slowest route of your site, not the slowest page necessarily, the slowest route.

Speaker 1

它能告诉我们,正斜杠加slug这个路由是最慢的。

It's able to see show us that the forward slash, show number forward slash slug is the slowest route.

Speaker 1

实际上,现在它是最慢的路由,但以前并不是,因为我们之后会谈到在那里所做的某些更改。

And, actually, that's the slowest route now, but it wasn't because we'll talk about some of the changes we made there.

Speaker 1

它还会告诉你各种信息,比如首次绘制的平均时间、机会评分,也就是提醒你:很多人访问这个页面,而它却很慢。

And it tells you all kinds of information, like the average time to first bite, the opportunity score, which is like, hey, a lot of people are hitting this thing and it is slow.

Speaker 1

因此,这里有很大的优化空间。

Therefore, there's a lot of opportunity here.

Speaker 1

对吧?

Right?

Speaker 1

正因为如此,我们能够准确地看出首先应该关注哪些方面,也就是那些最慢、最有潜力提速的页面。

And because of that, we were able to see exactly what are the things we should be focusing on first, which are the slowest pages, which have the most opportunity to speed up.

Speaker 1

我们发现网站一些关键页面的性能评分偏低,主要是首页和展示页,这两个是最突出的。

And we were seeing a handful of red perf scores for some of the key pages on the site, the index page and the show page primarily were the two biggest ones.

Speaker 1

这就是网页核心指标。

So that's the web vitals.

Speaker 1

对吧?

Right?

Speaker 1

你看到首次绘制时间了。

You're seeing time to first bite.

Speaker 1

你看到最大内容绘制(LCP)是多少吗?

You're seeing the what is the LCP largest?

Speaker 2

最大内容绘制。

Largest Contentful Paint.

Speaker 1

最大内容绘制、首次内容绘制、首次输入延迟、累计布局偏移。

Largest Contentful Paint, the first Contentful Paint, first input delay, cumulative layout shift.

Speaker 1

我们正在查看我们网站所有路由的这些信息。

We're seeing all of that information across all of the routes of our site.

Speaker 1

使用Sentry中的查询功能,我们还能查看所有的数据库查询,并识别出哪些数据库查询持续最慢。

Another thing that we're able to see using the queries feature within Sentry is we were able to look at all of our database queries and we're able to see which of our database queries were the slowest consistently.

Speaker 1

其中有几项查询明显持续缓慢。

And there were a handful of ones that stood out as consistently being slow.

Speaker 1

有了这些信息,你就掌握了网页性能指标、查询数据、Sentry中的整体性能功能,再加上直观体验。

And so with that information, you have the the web vitals, you have the queries, you have the general performance features here in Sentry, and then you have things like the eye test.

Speaker 1

你正在使用这个网站。

You're using the site.

Speaker 1

你感受到它有多慢。

You're feeling how slow it is.

Speaker 1

你可以精准地确定首先应该关注哪些最重要的问题,然后从那里开始。

You can really dial in exactly what are the biggest things you should be looking at first and then start there.

Speaker 1

因此,我们知道这里有一个数据库后,第一件事就是专门查看数据库调用。

So the first thing we did knowing that we have a database here was we looked into the database calls specifically.

Speaker 1

通过查询标签,我们能够看到,我发现了一堆非常慢的数据库查询,而且它们全部都是 Prisma 中的 findFirst 方法。

So using the queries tab, we're able to see, okay, I'm seeing that I have a bunch of really slow database queries and all of them were the find first method within Prisma.

Speaker 1

这让我想到了一件事。

And that made me think about something.

Speaker 1

这确实是我的第一个大型 Prisma 项目。

This is, you know, really my first big site with Prisma.

Speaker 1

我当时在想,findFirst,也许这个方法本身有问题。

And I was thinking find first, maybe there maybe there's an issue with find first.

Speaker 1

也许我们漏掉了某个索引之类的东西。

Maybe we're missing an index or something.

Speaker 1

结果发现,findFirst 其实就是带有限制为一的 find 方法。

Well, it turns out that find first is really just a find mini with a limit of one.

Speaker 1

而我们实际上应该使用的方法是 find unique,性能更好。

And the method that we actually wanted to use was find unique, better performance.

Speaker 1

这很容易解决。

That's an easy fix right there.

Speaker 1

所以我们把所有 find first 都改成了 find unique。

So we just changed all of our find first to find unique.

Speaker 1

简单修复,性能提升,不错。

Easy fix, performance win, nice.

Speaker 2

我们遇到的另一个问题是字幕被加载了。

So the other thing we had was the transcripts were being loaded.

Speaker 2

我们制作节目页面的方式是,当你访问 Syntax 上的某一集时,页面上会有播放器、该集的节目笔记以及其他一些内容。

The way that we did the show page so when you visit one of the episodes on Syntax, we just had like we had the player, we had the show notes for that specific show, and a couple of other items on that page.

Speaker 2

当需要实现字幕功能时,我想了想,好吧。

And then when it came time to implement the transcripts, what I did is I was like, okay.

Speaker 2

字幕不需要出现在每个页面上,所以它应该作为一个独立的标签页存在。

Like, the transcript doesn't need to be on every page, so it needs to be on its own, like a separate tab.

Speaker 2

对吧?

Right?

Speaker 2

所以我写了一些标签页,然后写了一个路由,用来判断你是处于节目页面,还是处于节目页面/字幕页面,然后根据情况隐藏或显示相应内容。

So I I wrote some tabs and then I wrote a route that would catch whether you're on the show page or if you're on the show page forward slash transcripts, then hide and show depending on on what we have there.

Speaker 2

然而,首先,在这一切发生之前,字幕文件本身非常庞大,因为

However, first of all, before any of this stuff happened, the transcripts itself are they're absolutely massive because

Speaker 1

是的。

Yeah.

Speaker 2

你拥有完整的字幕内容。

You have the entire transcript.

Speaker 2

而在这些字幕中,包含了一种叫做“语段”的内容,每个语段大概是一个词,或者一两句话。

And inside of that, you have what are called utterances, which is like a a word or, I don't know, two or three sentences.

Speaker 2

而在每个语段中,又包含了每一个单词。

And then inside of the utterances, you have every single word.

Speaker 2

它还会记录每个单词的开始和结束时间。

And it tells you when the words have started and stopped.

Speaker 2

我把所有这些数据都保存在数据库中,因为将来可能需要开发一个视频工具,用来高亮显示当前正在说的单词。

And I've saved all that data in the database because there may be a time when we wanna build like some sort of video tool that will you know, if you wanna highlight the currently spoken word.

Speaker 2

所以我心想,好吧,我们就把所有这些数据都保存下来。

So I was like, alright, we'll save all that data.

Speaker 2

在开发过程中,我加载了每一个单词的数据。

And I was loading the data for every single word just in in dev.

Speaker 2

但这实在太慢了,因为需要处理的 JSON 数据量太大了——想象一下一小时的对话,每个单词都有开始时间、结束时间、置信度和说话人 ID。

And it was just it's way too slow because the amount of JSON you need like, imagine an hour's worth of talking and every word has a start, a stop, a confidence, and a speaker ID attached to every single one.

Speaker 2

数据量实在太大了。

It's just way too big.

Speaker 2

所以我们放弃了高亮当前所说单词的功能,改为只获取每一个语句,然后根据具体时间判断当前正在说的语句。

So we I scrapped being able to highlight the currently spoken word, and I'm just getting the every single utterance that I'm able to figure out the currently spoken utterance depending on the the specific time.

Speaker 2

但我们设置标签的方式是,转录数据在你访问节目页面时才加载,这对性能来说是好的。

But the the way that we did the tabs was that the transcript data was being loaded on when you were visiting the actual show page, which is is is good for, like, performance.

Speaker 2

你切换过去时,加载时间却太长了。

You you flip over to it, but it's it takes way too long.

Speaker 2

有人访问页面并点击进入字幕的可能性很低。

And the chances of somebody visiting the page and clicking over to the transcript is is pretty low.

Speaker 2

对吧?

Right?

Speaker 2

大多数人可能不会去查看字幕标签页。

Most people are probably not going to the transcript tab.

Speaker 2

如果你去了,那也没关系。

And if you are, that's fine.

Speaker 2

在那个页面上多加载一点内容没问题,但其实没必要提前加载。

That's fine to have a bit of a larger load on that page, but it doesn't need to be loaded.

Speaker 2

当你访问主页面时,这些数据并不需要被加载。

That data doesn't need to be loaded when you visit just the actual page.

Speaker 2

所以你重新调整了 SvelteKit 的所有嵌套路由?

So you rejigged how SvelteKit did all the nested routes?

Speaker 1

是的。

Yeah.

Speaker 1

这并不是嵌套布局能力的证明。

And this isn't a testament to the the abilities of nested layout.

Speaker 1

所以,你知道,我们以前的做法是,在页面上一次性执行了两个查询,对吧?

So, you know, the way that we had it before was that both of the queries were being done in one fell swoop on the page, right?

Speaker 1

那是展示页面。

It was the show page.

Speaker 1

然后我们只是用一个if语句来显示或隐藏字幕。

And then we were just using like basically an if statement to show or hide the transcripts.

Speaker 1

对吧?

Right?

Speaker 1

我们意识到,并不是每个访问页面的人都会点击那个字幕标签来加载字幕内容。

And we realized that not everybody who hits the page is even gonna click on that transcripts transcripts tab to load those transcripts.

Speaker 1

那为什么我们要在页面初次加载时,为所有人做这些繁重的工作呢?

So why are we doing that heavy lifting for everybody on the initial load of the page?

Speaker 1

所以我们把大部分节目信息的数据库调用移到了布局本身,并将大部分HTML内容也移到了页面的布局中。

So what we did is we moved the database calls for most of the show information to the layout itself, and we moved most of the HTML into the layout of the page.

Speaker 1

通过嵌套布局路由,我们能够通过插槽传入显示节点或字幕,也就是根据当前激活的标签来决定。

Then using nested layout routing, we were able to pass in via a slot either the the show nodes or the transcript, basically, whichever tab there.

Speaker 1

因此,不再使用带有 if 语句的标签功能,而是通过嵌套布局路由来切换。

So instead of having that tab function with a if statement is being toggled with the nested layout route.

Speaker 1

同样,我们将字幕的数据库调用移到了字幕页面本身,也就是说,我们现在可以利用这个功能,将这些信息作为真正的嵌套路由包裹在其他信息之外。

And likewise, we moved the transcript database call to the page of the transcript page, basically saying, Hey, we are now able to use this feature where we can pull out and have this information wrapping some other information as a true nested route.

Speaker 1

这样一来,就像之前一样,你可以直接访问 /transcript 进入字幕页面,但只有在那时才会执行那个庞大的数据库查询,这为我们节省了大量时间。

And that way, just like before, you could hit forward slash transcript and get to the transcript page, but only then are you going to be having to do that massive DB call, which is going to save us a considerable amount of time and did.

Speaker 1

因此,我们在数据库层面做了这些重大改进,以提升最重页面的加载速度,仅这一项就带来了巨大影响。

So those were some big, big things that we did on the database side of things to improve loading on the heaviest pages, and it it that alone had a huge impact.

Speaker 2

我在数据库和按页面加载数据时经常遇到这种情况。

And I I I run into this quite a bit with databases and loading data per page.

Speaker 2

我几乎总是对自己说,真希望能把查询直接放在组件内部。

And I almost always tell myself, I wish I could put the query in the component itself.

Speaker 2

是的。

Yeah.

Speaker 2

知道吗?

Know?

Speaker 2

而且那正是

And that was

Speaker 1

是的。

Yeah.

Speaker 1

Have

Speaker 2

服务端组件。

server components.

Speaker 2

是的。

Yeah.

Speaker 2

使用 Apollo,他们就是这样做的。

With Apollo, they do that.

Speaker 2

比如,Apollo 会处理 Next。

Like, Apollo will take a Next.

Speaker 2

一个 JS 网站,遍历组件树来查找查询。

Js site and walk the component tree looking for queries.

Speaker 2

我认为 Quick 也是这样做的,比如

And I think Quick does that as well where

Speaker 1

你是在说 Apollo 吗?

Are you talking about Apollo?

Speaker 2

是的。

Yeah.

Speaker 2

Apollo,就像 Apollo GraphQL。

Apollo, like Apollo GraphQL.

Speaker 2

好的。

Okay.

Speaker 2

这样更好一点,因为你就不用在页面级别决定需要哪些数据了。

And that's a bit nicer because, like, then you don't have to decide at a page level what data you want.

Speaker 2

你可以直接把查询放在组件本身里,然后页面会自动判断:哦,我没有渲染这个组件,所以暂时不获取这个数据。

You could just put the query in the in the component itself, and then the page should figure out, oh, well, I'm I'm not rendering this component, so I'm not gonna fetch that just yet.

Speaker 2

所以我认为,许多这些框架在这方面都有可能得到改进。

So I think that's something that could possibly be improved with a lot of these frameworks.

Speaker 1

是的。

Yeah.

Speaker 1

你觉得 React 服务器组件是否直接解决了这个问题?

And do you think React server components does like solve that directly?

Speaker 1

因为对我来说,它确实解决了。

Because to me it does.

Speaker 1

我想知道其他人会不会也开始这么做,或者是否存在其他解决方案。

I wonder if everybody else is going to start to do that or if there's a different solution there.

Speaker 2

是的。

Yeah.

Speaker 2

我认为,直接在服务器上渲染并把 HTML 发送到客户端,能解决很多问题,也让组件更具可移植性。

I think the the idea of, yeah, just render it on the server and send the HTML to the client fixes a lot of that and it makes your components a lot more portable.

Speaker 1

是的,完全没错。

Yeah, totally.

Speaker 1

不错。

Cool.

Speaker 1

接下来我们做的另一件事是,说实话,如果你遇到性能问题,而且不是明显由客户端循环之类的因素导致的卡顿,我认为你的系统中很多性能问题都可以通过几件事来解决。

Well, next thing we did, which is, you know, honestly, I would say if you're running into perf issues, right, if it's not clearly like a a client side jank from looping or something like that, I would say so many perf issues in your stacks can be solved by a couple of things.

Speaker 1

优化数据库调用、添加索引,诸如此类的操作,然后是缓存。

Optimize optimizing database calls, adding indexes, those types of things, and then caching.

Speaker 1

缓存能解决很多性能问题,而且效果非常显著,因为我们的网站上一些最耗资源的列表都在频繁进行数据库调用。

Caching will solve so many perf issues and it will solve them really effectively because, you know, again, some of the heaviest lists that we had in the site were going off to do the database calls.

Speaker 1

如果这个数据库调用需要200毫秒,那就是在整体加载时间上额外增加了200毫秒。

Whether if that database call takes two hundred milliseconds, that's two hundred additional milliseconds you're tacking on to the entire load of things.

Speaker 1

如果耗时更长,那每次调用都会叠加这部分延迟。

If it takes longer than that, again, that's that's tacking on to every single time you do that call.

Speaker 1

所以你应该做的是缓存这些调用,这样就不用每次都访问数据库,而是直接从缓存中获取数据。

So what you wanna be doing is caching those calls so that way you're not having to hit the database all the time and what you're hitting is a cache.

Speaker 1

这个缓存可以是内存中的数据,也就是简单地保存为一个变量,也可以是像Redis这样的内存级存储。

And that cache can be something in memory, which is basically just saving it to a value, or it can be in a, a memory level store like a Redis store.

Speaker 1

我们使用的是一个叫 Upstash 的服务。

What we used is we used a service called Upstash.

Speaker 1

如果你听说 Vercel 正在推出他们自己的缓存平台,我认为它叫 KV。

Now if you've heard that Vercel is releasing their own caching platform, I believe it's called KV.

Speaker 1

你可能听说过 KV 其实是 Upstash 的一个封装。

You might have heard that KV is just a wrapper around Upstash.

Speaker 1

所以,虽然我们本可以使用 Vercel 的缓存平台并为此支付额外费用,但直接使用 Upstash 本身要合理得多。

So while we could have used Vercel's caching platform and paid the premium on top of it, it makes way more sense to just use Upstash itself.

Speaker 1

我的意思是,为什么不这么做呢?

I mean, why wouldn't you do that?

Speaker 1

对吧?

Right?

Speaker 1

尤其是当它们本质上是一样的时候。

Especially if it's the same.

Speaker 1

而且它基本上是使用 Redis,但通过无服务器 API 来实现。

And and basically it is using Redis, but it's using Redis with a serverless API.

Speaker 1

所以我们使用了 Upstash 及其库来实现 Upstash 缓存,这基本上与 Redis 的 API 完全相同。

So we were able to use Upstash, with their library to do the Upstash caching, which is basically the exact same as Redis' API.

Speaker 1

你用一个键来设置值,再用同一个键来获取它。

You set something with a a a key and you retrieve it with that key.

Speaker 1

这就是一个键值存储。

Key value store.

Speaker 1

其实就这么简单。

It's really just as simple as that.

Speaker 1

我们的方式是将每个节目的数据库调用结果保存到缓存中。

And the way we're doing it is we're saving each database call for each show into the cache.

Speaker 1

当我们查询时,首先检查缓存中是否有该节目。

And when we check, we first check the cache to see if the show's in the cache.

Speaker 1

如果缓存中有,就返回缓存中的数据。

If it's in the cache, we return the one in the cache.

Speaker 1

如果没有,就从数据库中获取。

If it's not, we we get the one from the database.

Speaker 1

所以你只在需要从数据库获取信息时才进行数据库调用。

So you're only doing the database calls when you have to get, information from the database.

Speaker 1

那么,什么时候必须从数据库获取信息呢?

Now when do have to get information from the database?

Speaker 1

我们是按毫秒级别的时间线来设置的。

Well, we set it up on a millisecond timeline.

Speaker 1

因此,新的节目会每六百毫秒自动从数据库获取最新数据。

So newer shows automatically will retrieve a new one from the database every six hundred milliseconds.

Speaker 1

当它从数据库获取新数据时,会同时更新缓存。

When it retrieves a new one from the database, it then updates the cache.

Speaker 1

对吧?

Right?

Speaker 1

而较旧的节目则会保留在缓存中长达三千六百毫秒,因为旧节目通常更新频率较低。

And likewise, older shows will be there for three thousand six hundred milliseconds that way because the older shows, you know, they don't get updated as much.

Speaker 1

假设我们的节目笔记中出现了错误,我们需要快速更新它。

And let's say we have an, uh-oh, in one of our show notes or something, we need to update it really quick.

Speaker 1

我们可以进行这个更新,并确保不是每个人都会收到旧的缓存数据。

We can make that update and be assured that not everybody's going to be served the old cache.

Speaker 1

另外,我在管理后台创建了一个清除全部缓存的按钮,Wes,我不知道你有没有看到,用于紧急情况。

Now I also created in the admin section, Wes, I don't know if you've seen this, a drop all cache button in an emergency.

Speaker 1

我们想彻底清除整个缓存。

We wanna just completely nuke the cache.

Speaker 1

你可以点击这个按钮。

You can click that button.

Speaker 1

它会直接删除整个缓存。

It's just gonna delete the cache entirely.

Speaker 2

哦,这很好。

Oh, that's good.

Speaker 2

我本来也在考虑这个问题。

I was gonna think about that.

Speaker 1

是的。

Yeah.

Speaker 1

我的意思是,我来自Drupal的世界,在那里每次做任何更改,你都会点击删除全部缓存的按钮,因为所有内容都被缓存得非常彻底。

I mean, I come from the world of Drupal where anytime you make any change, you click the delete all cache button because everything is cached so hard.

Speaker 1

是的。

Yeah.

Speaker 1

在Drupal的世界里,人人都知道缓存几乎总是问题的根源。

Everybody knows in in Drupal world that your cache is almost always going to be the problem.

Speaker 2

所以你知道,你也可以把构建缓存或最新的Git提交作为键的一部分。

So You You know, what you could also do is you could stick the, like, the build cache or the latest git commit as part of the key.

Speaker 2

嗯。

Mhmm.

Speaker 2

一旦你进行新的构建,我知道像Vercel这样的CDN服务通常都会采用类似的做法。

And then as soon as you do a new build, I know with, like, a lot of Vercel CDN CDN stuff, they do something similar to that.

Speaker 2

一旦你有了新构建,整个缓存就会失效。

As as soon as you have a new build, your entire cache is invalidated.

Speaker 2

通常来说,我不知道他们是不是这么做的,但你可以用某种唯一的标识符作为缓存键的一部分。

And and generally how I don't know if this is how they do it, but you can just use some sort of unique identifier key as as part of your key creation.

Speaker 1

是的。

Yes.

Speaker 1

这完全正确。

That's totally true.

Speaker 1

我们可以这样做来延长缓存的生命周期。

And we could do that to extend the lifespan of the cache.

Speaker 1

但同样,我们的数据更依赖于数据库,而不是网站的构建。

But again, our data is tied more to the database than it is the build of the site.

Speaker 1

对吧?

Right?

Speaker 2

没错。

Exactly.

Speaker 2

是的。

Yeah.

Speaker 2

你可能不希望这样。

You maybe don't want that.

Speaker 1

对。

Right.

Speaker 1

你可以使用文件的哈希值,也许不是哈希值,但你可以使用某种唯一标识符来表明这个节目已被更新,比如它最后更新的时间戳。

You could use the hash of the file, maybe not the hash, but you could use some sort of unique identifier to say that this show has been updated, maybe the timestamp for when it was last updated.

Speaker 1

也许这就是一个指标,对吧?

And maybe that's indicator, right?

Speaker 2

是的。

Yeah.

Speaker 2

但在大多数情况下,你只需要去喝杯咖啡,等你回来时问题就已经解决了。

But in most cases, just go grab a coffee and it will be fixed by the time you're done.

Speaker 2

对吧?

Right?

Speaker 2

比如,六百毫秒?

Like, six six hundred milliseconds?

Speaker 1

六百毫秒。

Six hundred millisecond.

Speaker 1

没什么。

Nothing.

Speaker 0

是的。

Yeah.

Speaker 2

所以你只缓存半秒钟?

So you're only caching it for half a second?

Speaker 1

哦,六百毫秒。

Oh, six hundred milliseconds.

Speaker 2

你是说六百六百秒吗?大概是。

Are you six hundred six hundred seconds, probably.

Speaker 1

是六百秒。

It's six hundred seconds.

Speaker 1

天啊。

Oh, man.

Speaker 1

这总是让我这样。

That always gets me.

Speaker 1

对不起。

I'm sorry.

Speaker 1

是六百秒。

It is six hundred seconds.

Speaker 1

我在我们的十分钟里写的是毫秒。

I wrote milliseconds in our Ten minutes.

Speaker 1

但其实是秒。

But it is seconds.

Speaker 1

是的。

Yes.

Speaker 1

十分钟。

Ten minutes.

Speaker 2

此外,Upstash 有一个很好的登录 API,如果你想知道这个东西是否被缓存了,可以直接查看。

Plus, Upstash has a nice, like, a nice login API where like you can literally if you're wondering, is this thing cached or not?

Speaker 2

以及这个东西的过期时间还剩多少?

And how much longer is left on the expiry of this thing?

Speaker 2

你可以直接登录 Upstash,他们提供一个简洁的图形界面,你可以查看并直接点击删除按钮,如果你真的想删除的话。

You can just log in to Upstash and they have a nice little GUI for you can you can go look at it and you can just click the delete button if you really want to.

Speaker 1

完全正确。

Totally.

Speaker 1

Upstash 的定价非常合理,这一点很棒。

And what's great about Upstash is the pricing was very reasonable.

Speaker 1

我的意思是,我们实际上没花多少钱,甚至还没达到我们所需支出的任何限额。

I mean, we're not spending really, we haven't even hit any sort of limits for what we need to be spending yet.

Speaker 1

所以从成本角度来看,它真的很好。

So cost wise, it's really good.

Speaker 1

我过去对为这个服务付费一直很犹豫,因为以前我习惯自己搭建一个私有的 Redis 服务器。

I was very apprehensive about paying for a service for this because in the past I'm used to spinning up a, like a private Redis server and going that way.

Speaker 1

但说实话,相比我之前在 Render.com 上运行私有 Redis 服务器为 Level Up 教程服务的开销,现在更便宜。

But honestly, has been cheaper than running a private Redis server on render.com where I did it before for level up tutorials.

Speaker 1

它确实比那更便宜。

It's it's been cheaper than that.

Speaker 1

所以,你知道的,嘿。

So, you know, hey.

Speaker 1

我再次担心这会以不健康的方式增加成本,但事实证明它非常节省成本。

I was concerned again that it would start to add to the cost in an unhealthy way, but it's been very cost effective.

Speaker 2

是的。

Yeah.

Speaker 2

当然,如果你在听这个播客,并且想在服务器端开发上更进一步,我建议你尝试自己实现一个 Redis 缓存。

Certainly, I would recommend if you're listening to this podcast and you're you're trying to get go a little bit further with your server side development is either a, try to implement a Redis cache yourself.

Speaker 2

这和你过去可能做过的许多本地存储设置没什么不同,或者尝试用 JavaScript 的 Map 构建一个简单的内存缓存,因为你经常可以这么做。

It's no different than a lot of, like, local storage settings that you've you've probably done in the past, or try build a little in memory cache with a map in JavaScript because you can I've you do that a lot.

Speaker 2

你可以在函数处理程序外部创建一个 Map,然后判断:如果这个 Map 包含某个键,就直接返回它。

You can create a map outside of your function handler and then just say, well, if if the map has this key, then return it.

Speaker 2

你还需要为数据添加时间戳,因为你得知道什么时候过期。

You do have to add like a timestamp to your data as well, because you need to know when to expire it.

Speaker 2

而这正是 Upstash 这个服务的便捷 API 之处。

And that's the sort of the nice API of this Upstash one.

Speaker 2

在 Radius 中,通常可以设置以秒为单位的过期时间。

And in Radius in general, can set an expiry in seconds.

Speaker 1

是的。

Yeah.

Speaker 1

这确实是一个很棒的功能,能够直接在这里设置过期时间。

That's actually a really great thing about this is being able to set that expiry in there.

Speaker 1

这样我就不用自己去计算了。

That way I don't have to do the math myself.

Speaker 1

对。

Yeah.

Speaker 1

我还写了一个辅助函数来处理这里的 Prisma 调用。

I also just wrote a helper function to do Prisma calls for this.

Speaker 1

所以我写了一个小的辅助函数,可以传入查询语句。

So I wrote a little helper function that you can pass in the query.

Speaker 1

你可以传入你想执行的函数,它会自动检查缓存,返回正确的结果,而无需你每次都手动操作。

You can pass in the the function you're trying to do, and it will automatically check the cache and return the right one or whatever without you having to do that manually each time.

Speaker 1

这只是一个不错的小小辅助函数。

It's just a nice little helper.

Speaker 2

添加了 if 语句。

Adding the if statements.

Speaker 1

是的。

Yeah.

Speaker 1

无需在代码中手动添加 if 语句来填充缓存逻辑。

Without adding the if statements populating your code.

Speaker 1

所以现在你只需要调用缓存函数,直接获取数据即可。

So now you just do a a cache function, cache call function, and and pull in that data directly in there.

Speaker 1

老实说,这真的非常好,查询速度立刻提升了。

And, honestly, it's been really super nice, and it it it's sped up the queries instantly.

Speaker 1

当然。

Absolutely.

Speaker 1

我们做的另一件事是,由于我们的网站是完全服务端渲染的,我们确保每个重型路由都设置了缓存头,并且对于新内容,我们采用了完全相同的策略:如果剧集是新的,缓存时间为六百秒。

Another thing we did is since our site is fully server side rendered, we just made sure that every single heavy route got cache headers, and we did the exact same strategy for if the episode is newer, it's going to be six hundred seconds.

Speaker 1

如果集数较旧,缓存时间将是3600秒。

If the episode is older, it's gonna be three thousand six hundred seconds in terms of how long it's cached for.

Speaker 1

我们还使用了stale-while-revalidate,如果你想了解更多,我们专门做了一期关于这个的节目。

And we also use stale wall revalidate as well, which if you wanna learn more about that, we did an entire episode on that.

Speaker 1

韦斯,你记得那期的编号吗?

Wes, do you have that episode number handy?

Speaker 1

让我看看。

Let me see.

Speaker 1

stale-while-revalidate,新Syntax搜索。

Stale man, the new Syntax search.

Speaker 1

让我告诉你。

Let me just tell you.

Speaker 1

新Syntax搜索速度非常快。

The new Syntax search is so fast.

Speaker 1

关于stale-while-revalidate,第100692期是一个非常好的课程,能帮你更深入地了解为什么你可能想使用它。

You need stale wall revalidate episode 100 692 is a really great lesson to learn a little bit more about why you might want to use that.

Speaker 1

无论如何,这些缓存机制加快了我们的初始加载速度,但我们确保每个需要快速加载的页面都启用了数据库缓存,并设置了正确且良好的缓存头,以确保这些页面加载迅速。

Either way, those those caching sped up our initial loading, but we just made sure that every single page that we needed to make sure was fast had database caching and had the correct and good cache headers for us to make sure that those were fast.

Speaker 1

没错。

Bingo.

Speaker 1

太棒了。

Bango.

Speaker 1

大量的速度优化。

Lots and lots of speed improvements.

Speaker 2

是的。

Yeah.

Speaker 2

在第692期节目中,当我讨论使用缓存验证时,我谈到了我们如何用它来缓存生成的OG图片。

In the episode six ninety two, stay while we're validated, I talked about how we're using it to cache our OG images that are generated.

Speaker 2

简单来说,我们使用Puppeteer加载网站中用于生成OG图片的部分。

So real quick, we use Puppeteer to load part of the website that generates the OG images.

Speaker 2

它会截取屏幕截图,然后将图片发送出去。

That takes a screenshot, and we'll send it out the other way.

Speaker 2

我知道现在有很多库可用。

I know there's lots of libraries out there.

Speaker 2

这绝对是最好的方法,我向你保证,如果你想要我们这样的控制权的话。

This is the absolute best way, I guarantee you, if you want the type of control that we want over it.

Speaker 2

不过,我在使用过程中遇到一个问题,就是与LinkedIn有关。

Now, I had hit one issue with that, and that was with LinkedIn.

Speaker 2

所以我注意到,每当我们把我们的节目分享到LinkedIn时,都看不到漂亮的开放图谱图片。

So I noticed that whenever we shared one of our episodes on LinkedIn, we weren't getting the really nice open graph images.

Speaker 2

而这些开放图谱图片在其他所有平台上都表现得非常好。

And those open graph images have been doing super well for us on all of the other platforms.

Speaker 2

我们收到了更多人点击,因为它们看起来太棒了。

We're getting a lot more traction of people clicking them because they they look awesome.

Speaker 2

对吧?

Right?

Speaker 2

但在LinkedIn上却无法正常显示。

And they weren't working on LinkedIn.

Speaker 2

所以我又深入研究了一整套复杂的问题,这真是我最不喜欢做的事——改点东西,提交,等构建完成,然后去我用的那个工具里查看。

So I went down the whole rabbit hole again, which is my, like, least favorite thing to do is to try change something, commit it, wait for the thing to build, go to the whatever tool that I'm using.

Speaker 2

这次我得用LinkedIn的调试工具,点那个按钮,让它抓取内容,然后给你显示一点关于它将如何展示的信息。

In in this case, I had to use the LinkedIn debugger tool and then press the button and it would scrape it and give you a little bit of information about what it's going to display.

Speaker 2

但它就是识别不了Open Graph标签。

And it would not pick up the Open Graph tags.

Speaker 2

于是我花了两个小时,尝试了各种标签、不同的Open Graph命名空间,还有name和description、meta这些参数的组合。

So I spent like like two hours trying different tags and different Open Graph namespaces and name versus description and meta.

Speaker 2

我跟Polypain聊过,因为他说他那边没问题,但当我真在LinkedIn上发布时,就是不行。

I was talking to from Polypain because like Polypain said it was working fine, but it wasn't working fine when I actually posted it on LinkedIn.

Speaker 2

最后我想,也许生成Open Graph图片需要八到十五秒,因为如果要启动一个新浏览器,大概要十五秒;如果不用启动浏览器,大概八秒就能完成截图。

And finally, I thought, like, wonder it it takes between eight and fifteen seconds to generate the open graph image because it has to fire up a fifteen seconds if it has to fire up a new browser, about eight seconds if it doesn't have to fire up the browser and take the thing.

Speaker 2

但等一张图片加载这么久,时间太长了。

And that's too long to wait for an image to load.

Speaker 2

对吧?

Right?

展开剩余字幕(还有 47 条)
Speaker 2

所以这些图片都是预先生成并缓存的。

So those those images are are pregenerated and cached.

Speaker 2

我使用了 stale-while-revalidate 头部,只是在第一个 CDN 节点上缓存它们。

And I was using stale while revalidate header to just cache them on the first cell CDN.

Speaker 2

对吧?

Right?

Speaker 2

过去,我实际上把这些值直接放在了自己的网站上。

In the past, I had actually stuck those values in my own website.

Speaker 2

我只是把它们存在内存里。

I stick them in just in memory.

Speaker 2

我会把它们缓存几个小时之类的,以前从来没出过问题。

I store them for a couple hours or whatever, I had never had an issue.

Speaker 2

但在 LinkedIn 上,不知为何,LinkedIn 就是不命中缓存。

But on LinkedIn, for some reason, LinkedIn would not hit the cache.

Speaker 2

于是我盯着 Vercel 的日志,尽管我已经确保在生成之前就已有 CDN 缓存,但 LinkedIn 总是会触发重新生成——不管他们是给 URL 加了参数,还是他们的用户代理不同,导致 Vercel 允许他们重新生成,我就是没法让他们命中缓存。

So I was watching the Vercel logs come in, and even though I guaranteed that there was a CDN hit from generating it beforehand, something with LinkedIn would always cause whether they were putting parameters on the end of the URL or whether sorry, not metategs, whether their user agent was different and Vercel was allowing them to regenerate it, I could not get them to hit the cache at all.

Speaker 2

这让人很沮丧,因为加载太慢了,LinkedIn一直告诉我找不到Open Graph图片。

That was frustrating because it was taking too long and LinkedIn was telling me it couldn't find the Open Graph image.

Speaker 2

所以我心想,嘿,斯科特,你把这个Redis东西加进来了。

So I was like, hey, Scott just put this Redis thing in here.

Speaker 2

我就打算用它。

I'm gonna use that.

Speaker 2

于是我直接拿了Redis,把图片存进去,并设置了一个合理的过期时间。

So I just grabbed Redis, threw the image into Redis, and gave it a decent expiry.

Speaker 2

下一次有人请求图片时,我就去Redis里查一下有没有。

And then next time when somebody requests the image, I check-in Redis if it is in there.

Speaker 2

我仍然保留了Vercel CDN的机制,因为我感觉在CDN层面缓存是最理想的。

I still left the, like, Vercel CDN stuff on there because I feel like that is ideal at a cache it at a CDN level.

Speaker 2

但为了解决LinkedIn的问题,我现在也把图片缓存在Redis里。

But to solve the LinkedIn issue, I'm now also caching it in Redis.

Speaker 2

在生成新的图片之前,我会先检查Redis里有没有。

And I check if it's in Redis before we go ahead and generate a fresh one in that.

Speaker 2

搞定。

Boom.

Speaker 2

问题解决了,部署了,现在LinkedIn上的Open Graph图片既美观又加载迅速。

That fixed it, deployed the sucker, and now we got nice open graph images, and fast on LinkedIn.

Speaker 1

不错。

Nice.

Speaker 1

所以,简而言之,整个事件的核心就是把数据丢进Redis。

So basically the long story short of this entire episode is dump it in Redis.

Speaker 1

你从负载最重的部分开始,把它们全扔进Redis,就这样。

You start with the heaviest loads and you dump them in Redis and then like, that's it.

Speaker 1

这就是整个策略。

That's the whole strategy there.

Speaker 1

所以,嘿,有效果。

So, hey, it works.

Speaker 2

好的。

All right.

Speaker 2

就这样了。

That's it.

Speaker 2

希望你学到了一些让网站更快的方法。

Hopefully you learned a thing or two about making the site faster.

Speaker 2

这还挺有意思的。

It's kinda interesting.

Speaker 2

我们过去经常谈论客户端性能,但这次根本没有涉及。

We've we've talked a lot about like client side performance in the past and we didn't have any.

Speaker 2

我觉得我们并没有遇到任何客户端性能问题,对吧?

I don't think we had any client side performance issues, did we?

Speaker 1

我觉得没有。

I don't really think so.

Speaker 1

没有。

No.

Speaker 1

你知道吗,我觉得网站的服务端渲染特性通常会让这些问题减轻一些。

I, you know, I think the server side rendering aspect of the site generally makes some of those things less heavy.

Speaker 1

你不需要太担心骨架屏的问题。

You're not having to worry about skeleton screens as much.

Speaker 1

我觉得有时候即使客户端的东西比较慢,也大多是数据加载的问题,而不是客户端负载重。

I I think sometimes maybe the the client side stuff that is even slow is still almost always data loading, but it's not heavy client.

Speaker 1

我们不需要去记忆化任何东西。

We we we didn't have to memoize things.

Speaker 1

我们不需要使用回调。

We didn't have to use callback.

Speaker 1

我们不需要做那些你可能得担心的任何事情。

We didn't have to do any of that stuff that you might have to worry about.

Speaker 1

是的。

Yeah.

Speaker 1

总的来说,客户端 JavaScript 的性能很好且简单。

In general, client side JavaScript performance was nice and easy.

Speaker 2

完美。

Beautiful.

Speaker 2

好的。

Alright.

Speaker 2

谢谢收听。

Thanks for tuning in.

Speaker 1

回头见。

Catch you later.

Speaker 1

再见。

Peace.

Speaker 1

再见。

Peace.

Speaker 1

前往 syntax.fm 查看我们所有节目的完整存档。

Head on over to syntax.fm for a full archive of all of our shows.

Speaker 1

别忘了在你的播客应用中订阅,或者如果你喜欢这个节目,留下评价。

And don't forget to subscribe in your podcast player or drop a review if you like this show.

关于 Bayt 播客

Bayt 提供中文+原文双语音频和字幕,帮助你打破语言障碍,轻松听懂全球优质播客。

继续浏览更多播客