django debug=false后静态文件丢失_Django性能指南:如何提升Django应用速度

在开发多款Django应用后,我学到了一些关于性能优化的知识。对于这些内容,无论前后端,都未曾详细记录,于是我决定在本文做个讨论。

如果您从未认真研究过Web应用程序的性能,或许在文中能够找到想要的答案。

为什么速度很重要

对网络而言,100ms的影响很大,1s则被认为是一个生命周期。无数研究表明更快的加载速度与更好的转换率、用户留存以及来自搜索引擎的自然流量息息相关。更重要的是,它提供了更好的用户体验。

不同应用,不同瓶颈

有许多技术和实践可以用于优化Web应用的性能,然而很容易被带到沟里,只有对症下药才能事半功倍。不同的Web应用有着不同的瓶颈,一旦解决,性能将得到质的飞跃。根据您的应用特点,找到适合的方案才是解决瓶颈问题的根本。

尽管本文针对Django开发人员,但这里提到的优化技巧也适用于其他技术栈。在前端方面,它对使用Heroku和无法访问CDN的开发人员尤为有用。

分析和调试性能问题

对于后端,我建议使用try-and-true django-debug-toolbar。它可以用于分析请求/响应周期,找到最耗时的部分。尤其有用的一点是它提供了数据库查询的执行时间,并在浏览器的独立窗口中提供了SQL EXPLAIN。

Google PageSpeed主要提供与前端相关的优化建议,不过有些也可用于后端(如服务器响应时间)。PageSpeed的分数与加载时间并不直接关联,但是可以让您清晰地了解应用的瓶颈所在。对于开发环境,可以使用Google Chrome’s Lighthouse,它提供相同的分析指标且可使用本地网络URI。此外,GTmetrix也是一个细节丰富的分析工具。

免责声明

也许有读者会认为本文的一些建议是错误的或者存在缺陷,这没关系,因为本文并不是终极指南,您可以按需选择合适的方案。

后端:数据库层

通常后端负责大部分繁重的工作,从此处开始优化是个不错的选择。

毫无疑问,这里我想先提到ORM的两个函数:select_related和prefetch_related。两者都专门用于处理检索相关的对象,并且通常通过最小化数据库查询次数来提升速度。

select_related

以音乐web-app为例,其model如下:

d7c22746e370806ce1aeca40f26ec350.png

每个艺术家只与一个唱片公司关联,而一个唱片公司可以签约多个艺术家:经典的一对多关系。每个艺术家可以发行多张唱片,每张唱片可以属于一个或多个艺术家。

我创建了一些假数据:

  • 20个唱片公司

  • 每个唱片公司有25个艺术家

  • 每个艺术家发行100首音乐作品

总体而言,这个小型数据库中有大约50500条记录。

下面,让我们实现一个标准函数来获取艺术家及他们的唱片公司。

django_query_analyze是我编写的装饰器,用于计算数据库查询次数和运行该函数的时间,其具体实现参见附录。

3bf0b0e55a7614064bbd14feb9c2f320.png

get_artists_and_labels是Django视图中一个常规函数,它返回一个列表,每个元素都包含艺术家的名字及其所在的唱片公司。我通过获取artist.label.name来强制评估Django QuerySet;您可以将其等同于尝试在Jinja模板中访问此对象。

39d09a46c2ab05683d47ba957f94cb56.png

下面运行此函数:

d30ebe34fc59461e3f0eeae653b26f92.png

在0.36s内,获取了500个艺术家及其唱片公司的信息。有趣的是,数据库被访问了501次。一次是查询所有的艺术家记录,另外500次,每一次都访问一个艺术家的唱片公司信息。这被称为“N+1问题”。下面使用select_related让Django在同一查询中检索每个艺术家的唱片公司。

84942034b1784853e2c608009c6bff83.png

运行此函数:

70aa0bc6086b6b4cf41eda59e47bee0f.png

发现减少了500次查询,且速度提高了96%。

prefetch_related

让我们来看另一个函数,用于获取每个艺术家发行的前100首音乐:

f9be1d2dc2dd4f6244e51748ac221ad2.png

对于100个艺术家,为每个艺术家获取他们发行的100只唱片需要多长时间呢?

da3333afeb219f29eb42b3190dc8d181.png

现在修改函数中的artists变量,添加select_related,这样也许会减少查询次数,使速度得到提升:

b6ad07de5e8bf511e2e4be0aac49e930.png

然而修改之后,会报出如下错误:

0e9e4292dde22228c5a2bded23e4cc8f.png

这是因为select_related只能用于缓存ForeignKey或OneToOneField属性。而Artist和MusicRelease之间是多对多关系,因此这里需要用到prefetch_ related:

c15ef8a5497852e8994b72b8ec5c670e.png

select_related只能缓存单方面的“一对多”关系,或者两边都是“一对一”关系。而prefetch_related可以用于其他场景,如多方面的“一对多”、“多对多”关系。如下是改进后的结果:

ff28bf5bdc808e55b02d6b75041af0a2.png

真棒!

使用select_related和prefetch_related需要注意以下几点:

  • 如果不建立数据库连接池,效果会更明显,因为减少了数据库的往返次数;

  • 如果结果集过大,使用prefetch_related反而会使速度变慢;

  • 对一个数据库查询不一定比两个或多个快。

索引

对数据库列建立索引可能会对查询性能产生很大的影响。既然如此,为何没在开篇就介绍此技巧呢?因为索引远比在模型字段上设置db_index=True复杂。

在常被访问的列上建立索引可以提高与其相关的查询速度。然而建立索引会付出额外的存储空间代价,因此需要权衡成本与收益。通常,创建索引会减慢插入/更新的速度。

只查询需要的内容

如果可以的话,请使用values(),尤其是values_list()来获取所需的数据库对象属性。继续前面的例子,如果只想展示艺术家们的名字,而不需要全部ORM对象的话,通常这样写查询会更好:

70e6e5bb163b236b02fddf10d67ff144.png

  • Haki Benita,一个真正的数据库专家(不像我),曾写过一些类似本节的内容,需要的话,请阅读Haki’s blog。

后端:请求层

下面来讨论一下请求层。这里包括Django视图、上下文处理器和中间件。此处正确的决策也会带来更好的性能。

分页

在之前的章节,我们用select_related函数返回了500个艺术家及其唱片的信息。许多情况下,一次返回这么多对象既不现实又不推荐。Django文档中关于分页的部分清楚地说明了Paginator对象的使用方法。如果您希望向用户返回的对象不多于N个,或者一次返回很多对象使您的应用变慢,请使用它。

异步执行/后台任务

某些场景有时会不可避免地消耗很多时间。例如,用户请求将大量数据从数据库中导出到XML文件。如果我们在同一进程中执行所有操作,其流程如下所示:

1df81c84fa7bb329b49a1aa36739daa6.png

假设处理此文件需要45s,我们不可能真的让用户等这么久。首先从UX角度,这是一种很可怕的用户体验。其次,如果应用在N秒后未进行正确的HTTP响应,某些主机实际上会结束此进程。

多数情况下,明智的做法是将其从请求-响应过程中剔除,放入另一个进程中:

736b0b7fe58c8c273c4c10263b7404da.png

后台任务不在本文的讨论范围,但是如果您需要执行上述操作,可以使用Celery库。

压缩Django的HTTP响应

  • 请勿将其与静态文件的压缩混淆,后者将在本文后续提到。

压缩Django的HTTP/JSON响应也可以减少延迟。具体是多少呢?来看一下未压缩的响应体的字节数:

5fabcc02facb811ec12f0a9473c0d766.png

大概67KB左右。我们能做的更好吗?许多开发者使用Django内置的GZipMiddleware进行gzip压缩。不过现在有一个更有效的工具brotli,它支持多种浏览器(当然,除了IE11)。

  • 重要提示:正如Django文档GZipMiddleware章节所述,压缩可能会导致您的网站出现安全漏洞。

接下来安装django-compression-middleware库。通过检查请求头的Accept-Encoding,它将选择浏览器支持的最快压缩机制:

b79b1664f1ba940448bbecd86ec87a90.png

将其包含到Django应用的中间件中:

ee41b13db3b565ecc76643621baed08c.png

再看一下响应体的字节数:

3484654ac3473477e682798b08238df7.png

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

缓存

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

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

在使用缓存之前,请确保已经对数据库或前端做了适当的优化。如果设计和查询得当,数据库可以快速且大规模地查询数据。

前端:处理更加繁琐

缩减静态资源大小可以大大加快Web应用程序的速度。即使已经做好了后端优化,不能高效地提供图片、CSS和JS文件也会降低应用程序的性能。

对于编译、精简、压缩和移除等问题,很容易迷失方向。下面让我们来理清楚这些内容。

提供静态文件

提供静态文件有多种方案。Django文档提供了Ngnix、Apache、Cloud/CDN或使用同一服务器等方案。

这里我采用了一种混合的方案:从CDN获取图片,将大型文件上传到S3,其他静态资源(如CSS、JS等)都是通过WhiteNoise处理的(稍后再详细介绍)。

概念

为了确保我们对某些问题的理解是一致的,我想对以下概念做一个解释:

  • 编译:如果您在样式表中使用了SCSS,则先需要将它们编译为CSS,因为浏览器不理解SCSS。

  • 精简:减少空格并删除CSS和JS文件中的注释可能会对大小产生重大影响。有时此过程会丑化程序:如将长变量名重命名为短变量名等等。

  • 压缩/合并:对于CSS和JS,意味着将多个文件合并为一个。对于图片,则表示删除一些数据以缩减文件大小。

  • 移除:删除无用代码。例如在CSS中删除未使用的选择器。

使用WhiteNoise提供静态文件

WhiteNoise允许Python Web应用程序自己提供静态资源。正如其作者所说,当其他方案如Nginx/Apache不可用时,就可以试试WhiteNoise了。

下面先安装它:

6bdd3830dcd9e4980134787d4e7fd96b.png

在启动WhiteNoise之前,请确保已在settings.py中配置了STATIC_ROOT:

5886905831f6c9810e31fce907456600.png

同时还需要在SecurityMiddleware下面配置WhiteNoise中间件:

20ed24fb2427c097f0bd8a7d898f174c.png

在生产环境中,需要运行manage.py collectstatic来启动。

尽管此步骤不是必需的,但强烈建议添加缓存和压缩:

b8918bc1f0ece0b0517a8a6638ccdbb5.png

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

还有一步也很重要:为了确保在开发环境和生产环境中的体验一致,请配置runserver_nostatic:

d7451cd163ffbe8f0e74f8792ef2f2ca.png

无论DEBUG的状态是否为True,都可添加此配置,因为通常不会在生产环境中通过runserver运行Django。

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

694bf0fde34a7c2beea45498d16022cb.png

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

6ba54757a16b7b0ea161ee0564d7adb2.png

因此,当再次部署应用时,静态文件将被覆盖且重命名,之前的缓存就无关紧要了。

使用django-compressor压缩

WhiteNoise已经具备压缩静态文件的功能,因此django-compressor是可选的。但是后者提供了额外的功能:合并文件。要想同时使用压缩器和WhiteNoise,需要一些额外的配置。

假设用户加载一个包含三个.css文件的HTML文档:

6c309679d0528189f464199e751e3d8d.png

浏览器将会发出三个不同的请求。多数情况下,在部署时合并这些文件会更加有效,django-compressor通过{% compress css %}模板标签来实现:

6fb9d51bed9c37edb441856d5e81dc93.png

这样就合并成如下内容:

3aee670c42862739f469fd715f6dc6ba.png

下面让django-compressor和WhiteNoise运行起来。安装:

1bb689627a093b8e77a9bcafbfe2dfc8.png

配置静态文件路径:

399681be1c5bc2bd34e6ed93deab563f.png

由于这两个库影响请求-响应周期,与默认配置不兼容,需要通过修改一些配置来克服此问题。

我比较习惯使用.env文件的环境变量,并且只创建一个settings.py,如果您习惯多配置文件,如settings/dev.py和settings/prod.py,您应该知道如何转化:

main_project/settings.py:

9f8dd6cf771b3141227cd55cf6fe4f18.png

COMPRESS_OFFLINE在生产环境中值为True,在开发环境中值为False。COMPRESS_ENABLED在两个环境中都为True。

在离线压缩时,必须在每次部署都运行manage.py compress。在Heroku上,您希望平台禁止自动执行collectstatic(默认是开启的),而在post_compile时才执行,那么在项目的根目录下创建bin文件夹,并创建post_compile文件做如下配置:

e52cf4aaaeb4232bd89284376dc807f0.png

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

b57f8fab5a243bea171b39db23c4c82a.png

精简CSS和JS

关于加载时间和带宽使用的另一个重要话题就是精简:通过删除空格和注释来(自动)缩减代码文件大小的过程。

解决此问题的方法有多种,如果您使用了django-compressor,只需在settings.py文件中做如下配置(当然其他支持此功能的压缩器也可):

a6c13b38008719842c50d490f2c57dc9.png

延迟加载JavaScript

影响性能的另一因素即加载外部脚本。其要点在于浏览器在解析页面其他内容之前,会先去尝试获取并执行

标签中的Javascript文件:

eee1cc26159bee37db92d1334a10a9d7.png

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

33abeca412aa2ec17fec65884566e866.png

两者都允许脚本异步获取。不同之处在于:使用前者时,一旦脚本下载完毕,会立刻终止其他HTML解析工作,优先执行脚本;而使用后者则会等所有解析工作完毕再执行。

关于async和defer的使用,我建议参考Flavio Copes的文章。通常可总结为:

  • 提高页面加载速度的最佳方法是在head中引入脚本,并为script标签添加defer属性。

懒加载图片

懒加载图片意味着进入客户端视区才请求它们。这样能节省用户的时间和带宽。目前有许多好用且无外部依赖的库如LazyLoad,确实没有理由不使用它们。此外,Chrome从76版本开始支持lazy属性。

LazyLoad使用起来非常简单,并且可定制化。在我自己的应用中,我希望它仅在具有lazy类的图像上应用,并在进入视区前开始加载300像素的图片:

be395f99afb1df6b7a6781cb3255475e.png

下面用已有图片尝试一下:

0f018505b0c117e7e7dca7d5c7e6e599.png

用src替换src属性,并添加lazy到class:

3bd322c25822b83de57e34e96b723107.png

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

如果某页面有很多图片,使用懒加载将大大减少加载时间。

优化和动态缩放图片

另一个需要关注的因素是图片优化。除压缩外,还有另外两个技术可以考虑。

首先,文件格式优化。与同质量的JPEG图片相比,新的WebP格式要小25%-30%。从2020年2月开始,一些浏览器开始支持此格式,不过还是需要提供标准格式图片备用。

其次,根据不同的屏幕尺寸提供不同的图片尺寸。如果某些移动设备的最大视区宽度为650px,那么为什么要和13英寸2560px显示器一样提供1050px的图片呢?

这里,您可以根据自己的app进行定制化处理。举个更简单的例子,可以使用srcset属性控制尺寸;并且如果想同时使用WebP、JPEG格式图片的话,可以使用元素绑定多个源。

如果您对上述内容感到困惑,可以参考这篇指南,里面对涉及的术语和示例给了很好的解释。

无用的CSS:删除导入

如果您正在使用Bootstrap之类的CSS框架,不要盲目地引入所有组件。实际上,一开始我会注释掉所有不必要的组件,并在需要时再添加。下面是bootstrap.scss中的一小段内容:

eea8919ae07cbc2b36d7aa5b45252b24.png

我不会使用诸如badges和jumbotron之类的功能,所以将其注释掉。

无用的CSS:使用PurgeCSS移除

一个更复杂的方法是使用类似PurgeCSS的库,它可以分析您的文件,检测CSS中无用的内容并删除。PurgeCSS是一个NPM软件包,因此如果您在Heroku上托管Django应用,需要安装Node.js buildpack。

结论

我希望您至少已经找到一个可以优化Django应用程序的方案了。如果您有任何疑问、建议或反馈,请随时在Twitter上给我留言。

附录

用于QuerySet性能分析的装饰器,如下是django_query_analyze装饰器的代码:

97d60ae257dae9febdbb9bf3cfe292cf.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值