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首音乐作品
下面,让我们实现一个标准函数来获取艺术家及,他们的唱片公司。
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响应
- 请勿将其与静态文件的压缩混淆,后者将在本文后续提到。
大概67KB左右。我们能做的更好吗?许多开发者使用django内置,的gzipmiddleware进行gzip压缩。不过现在有一个更,有效的工具brotli,它支持多种浏览器(当然,除了IE11)。
- 重要提示:正如django文档gzipmiddleware章节,所述,压缩可能会导致您的网站,出现安全漏洞。
将其包含到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允许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程序员
微信扫一扫关注公众号