Django性能指南:如何提升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如下:



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

我创建了一些假数据:

  • 20个唱片公司
  • 每个唱片公司有25个艺术家
  • 每个艺术家发行100首音乐作品
总体而言,这个小型数据库中有大约,50500条记录。

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

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



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



下面运行此函数:



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



运行此函数:



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

prefetch_related

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



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



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



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



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



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



真棒!

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

  • 如果不建立数据库连接池,效果会更明显,因为减少了数据库,的往返次数;
  • 如果结果集过大,使用prefetch_related反而会使,速度变慢;
  • 对一个数据库查询不一定比两个,或多个快。
索引

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

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

只查询需要的内容

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



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

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

分页

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

异步执行/后台任务

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



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

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



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

压缩Django的HTTP响应

  • 请勿将其与静态文件的压缩混淆,后者将在本文后续提到。
压缩Django的HTTP/json响应也,可以减少延迟。具体是多少呢?来看一下未压缩的,响应体的字节数:



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

  • 重要提示:正如django文档gzipmiddleware章节,所述,压缩可能会导致您的网站,出现安全漏洞。
接下来安装django-compression-middleware库。通过检查请求头的Accept-Encoding,它将选择浏览器支持,的最快压缩机制:



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



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



现在它的大小为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了。

下面先安装它:



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



同时还需要在securitymiddleware下面,配置whitenoise中间件:



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

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



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

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



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

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



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



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

使用django-compressor压缩

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

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



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



这样就合并成如下内容:



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



配置静态文件路径:



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

我比较习惯使用.env文件的环境变量,并且只创建一个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文件做如下配置:



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



精简CSS和JS

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

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



延迟加载JavaScript

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



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



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

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

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

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

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



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



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



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

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

优化和动态缩放图片

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

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

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

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

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

无用的CSS:删除导入 

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



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

无用的CSS:使用PurgeCSS移除

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

结论

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

附录

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



英文原文:https://openfolder.sh/django-faster-speed-tutorial
译者:我是昵称耶~


    关注 Python程序员


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册