性能优化篇


引言

据估计,如果一个网站加载时间超过3秒,那么将有40%的人放弃访问!因此,从商业角度来看,确保网站在3秒内加载完成非常重要。所以,每当我为新的 Laravel 项目编写代码时,我都尽量在我所给定的时间和成本限制内尽可能优化代码。如果我在现有的项目上工作,我也尝试用这些技巧更新任何运行缓慢的代码,以提高用户的整体体验。

在这篇教程中,我们将看到我使用(或建议其他开发者使用)的一些技巧,以便为我的客户和我自己的 Laravel 网站和应用程序获取一些快速的性能提升。

预期的读者

本篇教程面向任何阶段的 Laravel 开发者,希望快速轻松地增强他们应用程序的性能。

仅在数据库查询中获取需要的字段

优化 Laravel 网站的一个简单方法是减少应用和数据库之间传输的数据量。您可以通过在查询中仅指定您需求的列来实现这一点。

例如,假设你有一个包含20个不同字段的用户模型。现在,想象一下你的系统中有10000个用户,并且你正在尝试对每一个用户进行一些形式的处理。你的代码可能看起来像这样:

image-20230831160527426

上述查询将负责检索200,000个字段的数据。但是,想象一下,当你在处理每个用户时,你只实际使用 idfirst_namelast_name字段。所以,这意味着在你检索的20个字段中,有17个对于这段特定的代码来说基本上是多余的。所以,我们可以做的是明确定义在查询中返回的字段。在这种情况下,你的代码可能看起来像这样:

image-20230831160619002

通过这样做,我们将把查询返回的字段数量从200,000减少到30,000。虽然这很可能对数据库的 IO 负载没有多大影响,但它会减少应用和数据库之间的网络流量。这是因为会有(假定)比获取所有可用字段时更少的数据需要序列化、发送和反序列化。通过减少网络流量和需要处理的数据量,这将有助于提高你的 Laravel 网站的速度。

请注意,在上述示例中,你可能实际上永远不会这样做,你可能会根据情况使用分块分页。这个例子只是为了展示一个可能的,易于实现的解决方案。

这种解决方案可能对较小的网站或应用程序的性能提升不大。然而,它绝对可以帮助减少那些性能至关重要的应用程序的加载时间。如果你在查询一个包含 BLOBTEXT 字段的表,你可能会看到更好的改进。这些字段可能会保留大量的数据,从而可能增加查询时间。所以,如果你的模型包含这些字段,考虑在查询中明确定义你需要的字段,以减少加载时间。

在可能的地方使用预加载

当你从数据库获取任何模型,然后对模型的关联关系进行处理时,使用预加载是很重要的。预加载在 Laravel 中是非常简单的,基本上可以防止你在数据中遇到 N+1 问题。这个问题是由于对数据库进行 N+1 次查询引起的,N 是从数据库获取的项目的数量。为了更好地解释这个问题,并给出一些上下文,让我们看看下面的例子。

假设你有两个模型(CommentAuthor)之间有一对一的关系。现在,假设你有100条评论,并且你想要逐一查看它们,并输出作者的名字。

没有预加载的话,你的代码可能看起来像这样:

image-20230831161048315

上面的代码将导致数据库有 101 次查询。第一次查询是获取所有的评论。其余的一百次查询来自于在循环的每次迭代中获取作者的名字。显然,这可能会导致性能问题,导致你的应用慢下来。那我们如何改善这个问题呢?

通过使用预加载,我们可以将代码改为:

image-20230831161107600

如你所见,这段代码几乎一样,可读性依然。通过添加 ::with(authors),这将获取所有的评论,并且一次性获取作者。所以,这就意味着我们将查询从101次减少到2次!

有关更多信息,请查看 Laravel 关于预加载的文档

如何强制 Laravel 使用预加载

在 Laravel 中,你实际上可以防止懒加载。此功能非常有用,因为它可以帮助确保关联关系是预加载的。因此,它能够改善性能,减少对数据库的查询次数,如上述示例所示。

防止懒加载非常简单。我们只需要添加下面这行代码到我们的 AppServiceProviderboot() 方法:

image-20230831161245056

所以,在我们的 AppServiceProvider 中,它看起来会有点像这样:

image-20230831161257576

允许在生产环境中预加载

你可能只想在本地开发环境中使用此功能。这样,在你构建新功能的同时,可以提示你代码中使用懒加载的地方,但不会让你的生產网站完全崩溃。基于这个原因,preventLazyLoading() 方法接受一个布尔值作为参数。所以我们可以使用以下这行代码:

image-20230831161342348

因此,在我们的 AppServiceProvider 中,它可能会看起来像这样:

image-20230831161349467

通过这样做,如果你的 APP_ENV 是生产环境,这个特性将被禁用,以防任何懒加载查询滑过导致你的网站抛出异常。

如果我们尝试懒加载会发生什么?

如果我们在服务提供者中启用了该特性,并尝试在一个模型上懒加载关系,将会抛出一个Illuminate\Database\LazyLoadingViolationException 异常。

为了在这一点上进一步说明,让我们使用我们上面的 CommentAuthor 模型示例。假设我们启用了该功能。以下片段将抛出一个异常:

image-20230831161434239

然而,以下片段不会抛出异常:

image-20230831161448615

更多关于 Laravel 关联查询优化的技巧,参考 Laravel 数据库性能优化实战系列教程。

避免任何不需要或不想要的包

打开您的 composer.json 文件,并查看你的每个依赖项。对于你的每个依赖项,问你自己"我真的需要这个包吗?"。你的回答大多数时候将为是,但有些时候可能不是。

每次将一个新的 Composer 库加入到你的项目中,你都可能添加额外的可能不必要运行的代码。Laravel 的包通常包含在每个请求上运行的服务提供者,这些服务提供者注册服务并执行代码。因此,假设你在你的应用中添加了20个 Laravel 的包,那么在每个请求上可能至少有20个类被实例化并运行。虽然这不会对流量小的网站或应用的性能产生巨大影响,但在大型应用上你肯定会注意到差别。

解决这个问题的方法是确定你是否真正需要所有的包。也许你正在使用一个提供一系列特性的包,但你只在使用其一小部分特性。问你自己"我能不能自己编写这段代码并移除整个包?"当然,由于时间限制,不总是可以编写自己的代码,因为你将不得不编写、测试和维护它。至少通过使用包,你能利用开源社区为你做这些事。但是,如果一个包比较简单,快速地用你自己的代码替代它,那么我会考虑移除它。

缓存,缓存,缓存!

Laravel 提供了许多内置的缓存方法。这些方法可以让你在网站或应用程序上线运行时,无需做任何代码更改即可轻松地提速。

路由缓存

由于 Laravel 的运行方式,每个请求都会启动框架并解析路由文件。这需要读取文件、解析其内容,然后以你的应用程序可以使用和理解的方式保存它。所以,Laravel 提供了一个可以使用的命令,这个命令可以创建一个解析更快的单一路由文件:

php artisan route:cache

但请注意,如果你使用这个命令并更改你的路由,你需要确保运行:

php artisan route:clear

这将移除缓存的路由文件,以便你的新的路由可以被注册。如果你还没有的话,将这两个命令添加到你的部署脚本中可能是值得的。如果你不使用部署脚本,你可能会发现我创建的 Laravel Executor 包在部署时很有用。

配置缓存

类似于路由缓存,每次请求时,Laravel 都会启动,并读取并解析你项目中的每个配置文件。

因此,为了防止需要处理每个文件,你可以运行以下命令创建一个缓存的配置文件:

php artisan config:cache

与上面的路由缓存一样,每次你更新你的 .env 文件或配置文件时,你都需要记得运行以下命令:

php artisan route:clear

事件和视图缓存

Laravel 还提供了两个其他的命令,你可以使用这些命令来缓存你的视图和事件,以便在对你的应用程序做出请求时,它们都是预编译和准备好的。要缓存事件和视图,你可以使用以下命令:

php artisan event:cache

php artisan view:cache

和所有其他的缓存命令一样,当你对代码做出任何改变时,你需要记得通过运行以下命令来破坏这些缓存:

php artisan event:clear

php artisan view:clear

过去,我遇到过很多在本地开发环境中缓存配置的开发者,然后会花很长时间弄清楚为什么他们的 .env 文件的改变没有显示出来。所以,我可能只会建议在实时系统上缓存你的配置和路由,以便你不会遇到同样的情况。

查询和值的缓存

在你的 Laravel 应用程序的代码中,你可以缓存项目以改善网站的性能。例如,假设你有以下查询:

image-20230831162145230

为了在这个查询中使用缓存,你可以将代码更新为以下内容:

image-20230831162151790

上述代码使用了 remember() 方法。它在基本上做的就是它检查缓存中是否包含任何带有用户键的项目。如果有,那么它将返回缓存的值。如果在缓存中不存在,将返回 DB::table('users')->get() 查询的结果并且也会被缓存。在这个特殊的例子中,项目将会被缓存 120 秒。

像这样缓存数据和查询结果,可以是减少数据库调用,减少运行时间,和提高性能的一种非常有效的方式。然而,重要的是要记住有时你可能需要从缓存中删除不再有效的项目。

就上面的例子来说,假设我们缓存了用户的查询。现在假设一个新的用户已经被创建、更新或删除。那个缓存的查询结果将不再有效,也不再实时。为了解决这个问题,我们可以使用 Laravel 模型观察者从缓存中删除此项目。这意味着下次我们试图获取 $users 变量时,将会运行新的数据库查询,给我们最新的结果。

使用 PHP 的最新版本

每次 PHP 出一个新版本,性能和速度都会得到提升。Kinsta 对多个 PHP 版本和不同的平台(如 Laravel、WordPress、Drupal、Joomla)进行了大量测试,并发现 PHP 8.0 提供了最大的性能提升

相比于上述其他的提示,这个提示可能更难实现,因为你需要审查你的代码,以确保你可以安全地更新到 PHP 的最新版本。顺便一说,拥有一个自动化的测试套件可能会帮助你有信心做这个升级。

使用队列

这个建议可能需要比其他一些基于代码的技巧更长的时间来实施。尽管如此,这个建议在提升用户体验方面可能是最有成效的一个。

你可以通过使用 Laravel 的队列来减少性能时间。如果在你的控制器或类中有一些代码在一个请求中运行,但又不是特别需要在网页浏览器响应中显示,那么我们通常可以把它放入队列。

为了更容易理解,看一下这个例子:

image-20230831162553041

在上述代码中,当调用 store() 方法时,它会在数据库中存储联系表单的详情,发送一封电子邮件到一个地址,告知他们有新的联系表单提交,并返回一个 JSON 响应。这段代码的问题在于,用户必须等待电子邮件发送完成,才能在网络浏览器上接收到他们的响应。虽然这可能只需要几秒钟的时间,但它可能会导致访客离开。

为了使用队列系统,我们可以将代码修改为以下内容:

image-20230831162636042

上述 store() 方法中的代码现在会在数据库中存储联系表单的详情,将邮件加入队列等待发送,然后返回响应。一旦响应被发送回用户的网络浏览器,电子邮件就会被加入队列以便处理。通过这种方式,我们就不需要等待电子邮件发送完成才返回响应了。

查看 Laravel 的文档,获取更多关于如何为你的 Laravel 网站或应用设置队列的信息。更多关于 Laravel 队列使用和运维技巧,参考 Laravel 消息队列实战

小结

这篇教程为你提供了几种快速提升 Laravel 项目性能的方法,无需完全重构你的代码。当然,你还可以做更多的事情,但这些是我在需要快速提升性能时,通常喜欢使用的方法。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 控制器篇

>> 下一篇: 辅助函数篇