Django Speed Handbook:加快Django应用的速度

在开发多个Django应用程序的过程中,我学到了很多有关速度优化的知识。 此过程的某些部分,无论是后端还是前端,都没有详细记录。 我决定收集本文中我所知道的大部分内容。

如果您从未认真研究过网络应用的性能,那么一定会在这里找到不错的东西。

为什么速度很重要
不同的应用程序,不同的瓶颈
分析和调试性能问题
免责声明
后端:数据库层
select_related
prefetch_related
索引编制
只拿你需要的东西
后端:请求层
分页
异步执行/后台任务
压缩Django的HTTP响应
快取
前端:变得更毛茸茸
提供静态文件
词汇
使用WhiteNoise从Django提供静态文件
使用Django压缩器进行压缩和合并
缩小CSS和JS
延迟加载JavaScript
延迟加载图像
优化和动态缩放图像
未使用的CSS:删除导入
未使用的CSS:使用PurgeCSS清除CSS
结论
附录
用于QuerySet性能分析的装饰器

为什么速度很重要
在网络上,100毫秒可以带来很大的不同,而1秒钟是一生。无数的研究表明,更快的加载时间与更好的转化率,用户保留率以及来自搜索引擎的自然流量相关。最重要的是,它们提供了更好的用户体验。

不同的应用程序,不同的瓶颈
有许多技术和实践可以优化您的网络应用的性能。很容易被带走。寻找最高的工作量回报率。不同的Web应用程序具有不同的瓶颈,因此,当这些瓶颈得到解决时,它们将获得最大收益。根据您的应用程序,一些技巧将比其他技巧有用。

尽管本文是针对Django开发人员的,但此处的速度优化技巧几乎可以调整为任何堆栈。在前端方面,它对于使用Heroku托管的用户以及无法访问CDN服务的用户特别有用。

分析和调试性能问题
在后端,我推荐使用久经考验的django-debug-toolbar。它将帮助您分析请求/响应周期,并了解大部分时间花在了哪里。尤其有用,因为它提供了数据库查询的执行时间,并在浏览器中显示的单独窗格中提供了不错的SQL EXPLAIN。

Google PageSpeed将主要显示与前端有关的建议,但是有些建议也可以应用于后端(例如服务器响应时间)。 PageSpeed分数与加载时间并不直接相关,但应该可以让您清楚了解应用程序的低挂水果在哪里。在开发环境中,您可以使用Google Chrome浏览器的Lighthouse(灯塔),该灯塔提供相同的指标,但可以使用本地网络URI。 GTmetrix是另一个细节丰富的分析工具。

免责声明
有人会告诉您,这里的某些建议是错误的或缺乏的。没关系;这并不意味着要成为圣经或终极指南。将这些技术和技巧视作您可能会使用,不应该或必须使用的技巧。不同的需求要求不同的设置。

后端:数据库层
从后端开始是一个好主意,因为通常是在后台进行大部分繁重工作的层。

在我的脑海中,毫无疑问,我首先要提到的两个ORM功能是:select_related和prefetch_related。它们都专门处理检索相关对象,并且通常通过最小化数据库查询的数量来提高速度。

select_related
让我们以一个音乐网络应用为例,它可能具有以下模型:

因此,每个艺术家与一个唱片公司相关,并且每个唱片公司可以与多位艺术家签约:经典的一对多关系。 艺术家有许多音乐发行,每个发行可以属于一个或多个艺术家。

我创建了一些虚拟数据:

20条记录标签
每个唱片公司有25位艺术家
每个艺术家有100个音乐作品
总体而言,我们的小型数据库中有约50,500个这些对象。

现在,我们建立一个相当标准的功能,以吸引我们的艺术家和他们的唱片公司。 django_query_analyze是我编写的装饰器,用于计算数据库查询的数量和运行该函数的时间。 它的实现可以在附录中找到。

get_artists_and_labels是您可以在Django视图中使用的常规函数。 它会返回一个词典列表,每个词典都包含艺术家的姓名及其标签。 我正在访问artist.label.name来强制评估Django QuerySet; 您可以将其等同于尝试在Jinja模板中访问这些对象:

运行:

因此,我们在0.36秒内拉了500位艺术家及其标签,但更有趣的是,我们已经打了501次数据库。 对于所有艺术家,一次,再五百次:对每个艺术家的标签一次。 这称为“ N + 1问题”。 让我们告诉Django使用select_related在同一查询中检索每个艺术家的标签:

运行:

减少500个查询,速度提高96%。

prefetch_related
让我们看一下另一个功能,用于获取每位艺术家的前100首音乐作品:

为每个艺术家获取100位艺术家和100个发行版本需要多长时间?

让我们在此函数中更改artists变量并添加select_related,这样我们可以减少查询数量并有望提高速度:

如果您确实这样做,则会收到错误消息:

这是因为select_related仅可用于缓存ForeignKey或OneToOneField属性。 歌手与MusicRelease之间的关系是多对多的,这就是prefetch_ related出现的地方:

select_related只能缓存“一对多”关系的“一侧”,或“一对一”关系的任一侧。 您可以将prefetch_related用于所有其他缓存,包括一对多关系中的多面关系和多对多关系。 这是我们示例中的改进:

ok,

有关select_related和prefetch_related的注意事项:

如果您不建立数据库连接池,则由于往返数据库的次数减少,收益将更大。
对于非常大的结果集,运行prefetch_related实际上会使事情变慢。
一个数据库查询不一定比两个或多个查询更快。
索引编制
索引数据库列可能会对查询性能产生很大影响。那为什么不是本节的第一条款呢?因为索引比简单地在模型字段上分散db_index = True更为复杂。

在经常访问的列上创建索引可以提高与它们相关的查找速度。索引虽然要付出额外的写入和存储空间的代价,所以您应该始终衡量自己的收益:成本比率。通常,在表上创建索引会减慢插入/更新的速度。

只拿你需要的东西
如果可能,请使用values()尤其是values_list()来仅提取数据库对象的所需属性。继续我们的示例,如果我们只想显示一个艺术家名称列表并且不需要完整的ORM对象,通常最好这样编写查询:

后端:请求层
我们要看的下一层是请求层。 这些是您的Django视图,上下文处理器和中间件。 此处的正确决策也将带来更好的性能。

分页
在与select_related有关的部分中,我们使用该函数返回500个艺术家及其标签。 在许多情况下,返回这么多对象是不现实的或不希望的。 Django文档中有关分页的部分非常清楚如何使用Paginator对象。 如果您不希望向用户返回多于N个对象,或者这样做会使您的网络应用运行太慢,请使用它。

异步执行/后台任务
有时某些动作不可避免地会花费很多时间。 例如,用户请求将大量对象从数据库导出到XML文件。 如果我们在同一过程中进行所有操作,则流程如下所示:

假设处理此文件需要45秒。 您并不是真的要让用户一直等待等待响应。 首先,从UX的角度来看,这是一种可怕的体验,其次,因为如果您的应用在N秒后未以正确的HTTP响应进行响应,则某些主机实际上会缩短该过程。

在大多数情况下,明智的做法是从请求-响应循环中删除此功能,并将其中继到其他进程:

后台任务不在本文的讨论范围之内,但是如果您需要执行上述操作,我相信您已经听说过Celery之类的库。

压缩Django的HTTP响应
请勿将其与静态文件压缩混淆,后者将在本文后面提到。

压缩Django的HTTP / JSON响应还可以节省用户的延迟。 多少钱 让我们检查一下响应正文中没有经过任何压缩的字节数:

因此,我们的HTTP响应约为67KB。 我们可以做得更好吗? 许多人使用Django的内置GZipMiddleware进行gzip压缩,但是如今,更新,更有效的brotli在所有浏览器中都享有相同的支持(当然,除了IE11)。

重要提示:如Django文档的GZipMiddleware部分所述,压缩可能会打开您的网站,使其违反安全性。

让我们安装出色的django-compression-middleware库。 通过检查请求的Accept-Encoding标头,它将选择浏览器支持的最快压缩机制:

将其包含在我们的Django应用的中间件中:

然后再次检查body的Content-Length:

主体大小现在为7.24KB,缩小了89%。您当然可以辩称,这种操作应该委托给专用服务器,例如Ngnix或Apache。我认为一切都是简单性与资源之间的平衡。

CACHE
缓存是存储特定计算结果以加快将来检索速度的过程。 Django具有出色的缓存框架,可让您在各种级别上使用不同的存储后端进行此操作。

在数据驱动型应用中,缓存可能会很棘手:您永远都不想缓存本应始终显示最新实时信息的页面。因此,最大的挑战不是设置缓存,而是确定要缓存的内容,持续多长时间以及了解何时或如何使缓存无效。

在使用缓存之前,请确保您已经在数据库级别和/或前端进行了适当的优化。如果设计和查询得当,数据库可笑地快速大规模地提取相关信息。

前端:变得更毛茸茸
减少静态文件/资产的大小可以大大加快您的Web应用程序的速度。即使您已正确完成了后端的所有操作,但低效地提供图像,CSS和JavaScript文件也会降低应用程序的速度。

在编译,最小化,压缩和清除之间,很容易迷路。让我们尽量不要。

提供静态文件
您可以在何处以及如何提供静态文件方面有多种选择。 Django的文档提到了运行Ngnix和Apache的专用服务器,Cloud / CDN或同一服务器方法。

我已经采取了一种混合的态度:从CDN提供图像,将大型文件上传到S3,但是其他静态资产(CSS,JavaScript等)的所有服务和处理都是使用WhiteNoise(已发现)完成的。稍后再详细介绍)。

词汇
为了确保我们在同一页面上,这就是我说的意思:

编译:如果您在样式表中使用SCSS,则首先必须将其编译为CSS,因为浏览器不了解SCSS。
缩小:减少空格并从CSS和JS文件中删除注释可能会对它们的大小产生重大影响。有时,这个过程涉及丑陋的事情:将长变量名重命名为短变量名,等等。
压缩/合并:对于CSS和JS,将多个文件合并为一个。对于图像,通常意味着从图像中删除一些数据以减小其文件大小。
清除:删除不需要/不需要的代码。例如,在CSS中:删除未使用的选择器。
使用WhiteNoise从Django提供静态文件
WhiteNoise允许您的Python Web应用程序自己提供静态资产。如其作者所述,当其他选项(例如Nginx / Apache)不可用或不受欢迎时,它就会出现。

让我们安装它:

在启用WhiteNoise之前,请确保已在settings.py中定义了您的STATIC_ROOT:

要启用WhiteNoise,请在settings.py中的SecurityMiddleware下方添加其WhiteNoise中间件:

在生产中,您必须运行manage.py collectstatic才能使WhiteNoise正常工作。

虽然此步骤不是必须的,但强烈建议您添加缓存和压缩:

现在,只要在模板中遇到{%static%}标签,WhiteNoise都会为您压缩和缓存文件。 它还负责缓存无效化。

另一个重要步骤:为了确保我们在开发和生产环境之间获得一致的体验,我们添加了runserver_nostatic:

不论DEBUG是否为True,都可以添加此代码,因为您通常不会在生产环境中通过runserver运行Django。

我发现增加缓存时间也很有用:

这会不会导致缓存失效问题? 否,因为在运行collectstatic时WhiteNoise会创建版本化文件:

因此,当您再次部署应用程序时,您的静态文件将被覆盖并具有不同的名称,因此以前的缓存变得无关紧要。

使用Django压缩器进行压缩和合并
WhiteNoise已经压缩了静态文件,因此django-compressor是可选的。 但是后者提供了额外的增强功能:合并文件。 要将压缩器与WhiteNoise一起使用,我们必须采取一些额外的步骤。

假设用户加载了一个HTML文档,该文档链接了三个.css文件:

您的浏览器将对这些位置发出三个不同的请求。 在许多情况下,部署时合并这些不同的文件会更有效,而django-compressor可以使用其{%compress css%}模板标记来做到这一点:

这个:

变成:

让我们来看一下使django-compressor和WhiteNoise发挥出色的步骤。 安装:

告诉压缩机在哪里可以找到静态文件:

由于这两个库截取请求-响应周期的方式,它们与它们的默认配置不兼容。 我们可以通过修改某些设置来克服这一问题。

我更喜欢在.env文件中使用环境变量,并使用一个Django settings.py,但是如果您具有settings / dev.py和settings / prod.py,您将知道如何转换这些值:

main_project / settings.py:

COMPRESS_OFFLINE在生产中为True,在开发中为False。 COMPRESS_ENABLED均为True。

使用脱机压缩时,必须在每个部署上运行manage.py compress。 在Heroku上,您将要禁止该平台为您自动运行collectstatic(默认情况下处于启用状态),而是选择在post_compile钩子中执行此操作,该钩子将在您部署时运行。 如果还没有,请在项目的根目录下创建一个名为bin的文件夹,并在其中创建一个名为post_compile的文件,其中包含以下内容:

Compressor的另一个好处是它可以压缩SCSS / SASS文件:

缩小CSS和JS
讨论加载时间和带宽使用情况时要应用的另一重要事项是最小化:通过消除空格和删除注释来(自动)减小代码文件大小的过程。

这里有几种方法,但如果您专门使用django-compressor,则也可以免费获得。 您只需将以下内容(或压缩器支持的其他任何过滤器)添加到settings.py文件中:

延迟加载JavaScript
导致性能降低的另一件事是加载外部脚本。 其要点是,浏览器将在遇到<head>标记时以及解析页面其余部分之前尝试获取并执行<head>标记中的JavaScript文件:

我们可以使用async和defer关键字来减轻这种情况:

异步和延迟都允许异步获取脚本而不会阻塞。它们之间的主要区别之一是允许执行脚本的时间:使用异步后,一旦下载了脚本,所有解析都将暂停,直到脚本执行完毕,而使用defer则仅在所有HTML完成后才执行脚本。解析。

我建议参考Flavio Copes在defer和aysnc关键字上的文章。其一般结论是:

使用脚本时,加快页面加载速度的最佳方法是将其放在头部,并在脚本标记中添加defer属性。

延迟加载图像
延迟加载图片意味着我们仅在它们进入客户(用户)视口之前或之后才请求它们。它为您的用户节省了时间和带宽(在蜂窝网络上为$)。有了出色的,无依赖的JavaScript库(例如LazyLoad),确实没有任何理由不延迟加载图像。此外,自版本76起,谷歌浏览器就本机支持lazy属性。

使用前面提到的LazyLoad非常简单,并且该库是非常可定制的。在我自己的应用中,我希望它仅适用于具有懒惰类的图像,并在进入视口之前开始加载300像素的图像:

现在,使用现有图像尝试一下:

我们用data-src替换src属性,并将lazy添加到class属性:

现在,当该图像在视口下为300像素时,客户端将请求此图像。

如果某些页面上有很多图像,则使用延迟加载将大大缩短加载时间。

优化和动态缩放图像
要考虑的另一件事是图像优化。除了压缩以外,这里还有另外两种技术需要考虑。

首先,文件格式优化。像WebP这样的较新格式在相同质量的情况下大概比普通JPEG图像小25-30%。从02/2020开始,WebP具有不错的浏览器支持,但不完整,因此如果要使用它,则必须提供标准格式的备用。

其次,为不同的屏幕尺寸提供不同的图像尺寸:如果某些移动设备的最大视口宽度为650px,那么为什么要为13英寸2560px视网膜显示屏提供与1050px相同的图像?

您也可以在此处选择适合您的应用程序的粒度和自定义级别。对于更简单的情况,可以使用srcset属性控制大小并以此来完成操作,但是例如,如果您还为同一图像提供具有JPEG后备功能的WebP,则可以将<picture>元素与多个来源和来源一起使用集。

如果上述内容对您和我一样使您感到复杂,则本指南应有助于解释术语和用例。

未使用的CSS:删除导入
如果您使用的是Bootstrap之类的CSS框架,则不要盲目地包含其所有组件。实际上,我将从注释掉所有不必要的组件开始,并仅在需要时逐渐添加这些组件。这是我的bootstrap.scss的一小段,其中所有不同部分均已导入:

我不使用徽章或巨型机这样的东西,因此可以放心地将其注释掉。

未使用的CSS:使用PurgeCSS清除CSS
一种更积极,更复杂的方法是使用PurgeCSS之类的库,该库可分析文件,检测未使用的CSS内容并将其删除。 PurgeCSS是一个NPM软件包,因此,如果要在Heroku上托管Django,则需要与Python并行安装Node.js buildpack。

结论
希望您已经找到至少一个可以加快Django应用速度的领域。 如果您有任何疑问,建议或反馈,请随时在Twitter上与我联系。

附录
用于QuerySet性能分析的装饰器
以下是django_query_analyze装饰器的代码:

原文:https://openfolder.sh/django-faster-speed-tutorial