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如下:

Django性能指南:如何提升Django應用速度

每個藝術家只與一個唱片公司關聯,而一個唱片公司可以簽約多個藝術家:經典的一對多關係。每個藝術家可以發行多張唱片,每張唱片可以屬於一個或多個藝術家。


我創建了一些假數據:

  • 20個唱片公司

  • 每個唱片公司有25個藝術家

  • 每個藝術家發行100首音樂作品


總體而言,這個小型數據庫中有大約50500條記錄。


下面,讓我們實現一個標準函數來獲取藝術家及他們的唱片公司。


django_query_analyze是我編寫的裝飾器,用於計算數據庫查詢次數和運行該函數的時間,其具體實現參見附錄。

Django性能指南:如何提升Django應用速度

get_artists_and_labels是Django視圖中一個常規函數,它返回一個列表,每個元素都包含藝術家的名字及其所在的唱片公司。我通過獲取artist.label.name來強制評估Django QuerySet;您可以將其等同於嘗試在Jinja模板中訪問此對象。

Django性能指南:如何提升Django應用速度

下面運行此函數:

Django性能指南:如何提升Django應用速度

在0.36s內,獲取了500個藝術家及其唱片公司的信息。有趣的是,數據庫被訪問了501次。一次是查詢所有的藝術家記錄,另外500次,每一次都訪問一個藝術家的唱片公司信息。這被稱為“N+1問題”。下面使用select_related讓Django在同一查詢中檢索每個藝術家的唱片公司。

Django性能指南:如何提升Django應用速度

運行此函數:

Django性能指南:如何提升Django應用速度

發現減少了500次查詢,且速度提高了96%。


prefetch_related


讓我們來看另一個函數,用於獲取每個藝術家發行的前100首音樂:

Django性能指南:如何提升Django應用速度

對於100個藝術家,為每個藝術家獲取他們發行的100只唱片需要多長時間呢?

Django性能指南:如何提升Django應用速度

現在修改函數中的artists變量,添加select_related,這樣也許會減少查詢次數,使速度得到提升:

Django性能指南:如何提升Django應用速度

然而修改之後,會報出如下錯誤:

Django性能指南:如何提升Django應用速度

這是因為select_related只能用於緩存ForeignKey或OneToOneField屬性。而Artist和MusicRelease之間是多對多關係,因此這裡需要用到prefetch_ related:

Django性能指南:如何提升Django應用速度

select_related只能緩存單方面的“一對多”關係,或者兩邊都是“一對一”關係。而prefetch_related可以用於其他場景,如多方面的“一對多”、“多對多”關係。如下是改進後的結果:

Django性能指南:如何提升Django應用速度

真棒!


使用select_related和prefetch_related需要注意以下幾點:

  • 如果不建立數據庫連接池,效果會更明顯,因為減少了數據庫的往返次數;

  • 如果結果集過大,使用prefetch_related反而會使速度變慢;

  • 對一個數據庫查詢不一定比兩個或多個快。


索引


對數據庫列建立索引可能會對查詢性能產生很大的影響。既然如此,為何沒在開篇就介紹此技巧呢?因為索引遠比在模型字段上設置db_index=True複雜。


在常被訪問的列上建立索引可以提高與其相關的查詢速度。然而建立索引會付出額外的存儲空間代價,因此需要權衡成本與收益。通常,創建索引會減慢插入/更新的速度。


只查詢需要的內容


如果可以的話,請使用values(),尤其是values_list()來獲取所需的數據庫對象屬性。繼續前面的例子,如果只想展示藝術家們的名字,而不需要全部ORM對象的話,通常這樣寫查詢會更好:

Django性能指南:如何提升Django應用速度

  • Haki Benita,一個真正的數據庫專家(不像我),曾寫過一些類似本節的內容,需要的話,請閱讀Haki’s blog。


後端:請求層


下面來討論一下請求層。這裡包括Django視圖、上下文處理器和中間件。此處正確的決策也會帶來更好的性能。


分頁


在之前的章節,我們用select_related函數返回了500個藝術家及其唱片的信息。許多情況下,一次返回這麼多對象既不現實又不推薦。Django文檔中關於分頁的部分清楚地說明了Paginator對象的使用方法。如果您希望向用戶返回的對象不多於N個,或者一次返回很多對象使您的應用變慢,請使用它。


異步執行/後臺任務


某些場景有時會不可避免地消耗很多時間。例如,用戶請求將大量數據從數據庫中導出到XML文件。如果我們在同一進程中執行所有操作,其流程如下所示:

Django性能指南:如何提升Django應用速度

假設處理此文件需要45s,我們不可能真的讓用戶等這麼久。首先從UX角度,這是一種很可怕的用戶體驗。其次,如果應用在N秒後未進行正確的HTTP響應,某些主機實際上會結束此進程。


多數情況下,明智的做法是將其從請求-響應過程中剔除,放入另一個進程中:

Django性能指南:如何提升Django應用速度

後臺任務不在本文的討論範圍,但是如果您需要執行上述操作,可以使用Celery庫。


壓縮Django的HTTP響應

  • 請勿將其與靜態文件的壓縮混淆,後者將在本文後續提到。


壓縮Django的HTTP/JSON響應也可以減少延遲。具體是多少呢?來看一下未壓縮的響應體的字節數:

Django性能指南:如何提升Django應用速度

大概67KB左右。我們能做的更好嗎?許多開發者使用Django內置的GZipMiddleware進行gzip壓縮。不過現在有一個更有效的工具brotli,它支持多種瀏覽器(當然,除了IE11)。


  • 重要提示:正如Django文檔GZipMiddleware章節所述,壓縮可能會導致您的網站出現安全漏洞。


接下來安裝django-compression-middleware庫。通過檢查請求頭的Accept-Encoding,它將選擇瀏覽器支持的最快壓縮機制:

Django性能指南:如何提升Django應用速度

將其包含到Django應用的中間件中:

Django性能指南:如何提升Django應用速度

再看一下響應體的字節數:

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


下面先安裝它:

Django性能指南:如何提升Django應用速度

在啟動WhiteNoise之前,請確保已在settings.py中配置了STATIC_ROOT:

Django性能指南:如何提升Django應用速度

同時還需要在SecurityMiddleware下面配置WhiteNoise中間件:

Django性能指南:如何提升Django應用速度

在生產環境中,需要運行manage.py collectstatic來啟動。


儘管此步驟不是必需的,但強烈建議添加緩存和壓縮:

Django性能指南:如何提升Django應用速度

這樣只要在模板中遇到{% static %}標籤,WhiteNoise會為您壓縮和緩存文件,此外它還負責緩存無效化。


還有一步也很重要:為了確保在開發環境和生產環境中的體驗一致,請配置runserver_nostatic:

Django性能指南:如何提升Django應用速度

無論DEBUG的狀態是否為True,都可添加此配置,因為通常不會在生產環境中通過runserver運行Django。


我發現增加緩存時間也很有用:

Django性能指南:如何提升Django應用速度

這不會導致緩存無效化的問題嗎?不會,因為在運行collectstatic時,WhiteNoise會創建版本化的文件:

Django性能指南:如何提升Django應用速度

因此,當再次部署應用時,靜態文件將被覆蓋且重命名,之前的緩存就無關緊要了。


使用django-compressor壓縮


WhiteNoise已經具備壓縮靜態文件的功能,因此django-compressor是可選的。但是後者提供了額外的功能:合併文件。要想同時使用壓縮器和WhiteNoise,需要一些額外的配置。


假設用戶加載一個包含三個.css文件的HTML文檔:

Django性能指南:如何提升Django應用速度

瀏覽器將會發出三個不同的請求。多數情況下,在部署時合併這些文件會更加有效,django-compressor通過{% compress css %}模板標籤來實現:

Django性能指南:如何提升Django應用速度

這樣就合併成如下內容:

Django性能指南:如何提升Django應用速度

下面讓django-compressor和WhiteNoise運行起來。安裝:

Django性能指南:如何提升Django應用速度

配置靜態文件路徑:

Django性能指南:如何提升Django應用速度

由於這兩個庫影響請求-響應週期,與默認配置不兼容,需要通過修改一些配置來克服此問題。


我比較習慣使用.env文件的環境變量,並且只創建一個settings.py,如果您習慣多配置文件,如settings/dev.py和settings/prod.py,您應該知道如何轉化:


main_project/settings.py:

Django性能指南:如何提升Django應用速度

COMPRESS_OFFLINE在生產環境中值為True,在開發環境中值為False。COMPRESS_ENABLED在兩個環境中都為True。


在離線壓縮時,必須在每次部署都運行manage.py compress。在Heroku上,您希望平臺禁止自動執行collectstatic(默認是開啟的),而在post_compile時才執行,那麼在項目的根目錄下創建bin文件夾,並創建post_compile文件做如下配置:

Django性能指南:如何提升Django應用速度

壓縮器的另一個好處是它可以壓縮SCSS/SASS文件:

Django性能指南:如何提升Django應用速度


精簡CSS和JS


關於加載時間和帶寬使用的另一個重要話題就是精簡:通過刪除空格和註釋來(自動)縮減代碼文件大小的過程。


解決此問題的方法有多種,如果您使用了django-compressor,只需在settings.py文件中做如下配置(當然其他支持此功能的壓縮器也可):

Django性能指南:如何提升Django應用速度


延遲加載JavaScript


影響性能的另一因素即加載外部腳本。其要點在於瀏覽器在解析頁面其他內容之前,會先去嘗試獲取並執行

標籤中的Javascript文件:

Django性能指南:如何提升Django應用速度

我們可以使用async和defer關鍵字來緩解這種情況:

Django性能指南:如何提升Django應用速度

兩者都允許腳本異步獲取。不同之處在於:使用前者時,一旦腳本下載完畢,會立刻終止其他HTML解析工作,優先執行腳本;而使用後者則會等所有解析工作完畢再執行。


關於async和defer的使用,我建議參考Flavio Copes的文章。通常可總結為:

  • 提高頁面加載速度的最佳方法是在head中引入腳本,併為script標籤添加defer屬性。


懶加載圖片


懶加載圖片意味著進入客戶端視區才請求它們。這樣能節省用戶的時間和帶寬。目前有許多好用且無外部依賴的庫如LazyLoad,確實沒有理由不使用它們。此外,Chrome從76版本開始支持lazy屬性。


LazyLoad使用起來非常簡單,並且可定製化。在我自己的應用中,我希望它僅在具有lazy類的圖像上應用,並在進入視區前開始加載300像素的圖片:

Django性能指南:如何提升Django應用速度

下面用已有圖片嘗試一下:

Django性能指南:如何提升Django應用速度

用src替換src屬性,並添加lazy到class:

Django性能指南:如何提升Django應用速度

現在,當該圖片在視區下為300像素時,客戶端將請求此圖片。


如果某頁面有很多圖片,使用懶加載將大大減少加載時間。


優化和動態縮放圖片


另一個需要關注的因素是圖片優化。除壓縮外,還有另外兩個技術可以考慮。


首先,文件格式優化。與同質量的JPEG圖片相比,新的WebP格式要小25%-30%。從2020年2月開始,一些瀏覽器開始支持此格式,不過還是需要提供標準格式圖片備用。


其次,根據不同的屏幕尺寸提供不同的圖片尺寸。如果某些移動設備的最大視區寬度為650px,那麼為什麼要和13英寸2560px顯示器一樣提供1050px的圖片呢?


這裡,您可以根據自己的app進行定製化處理。舉個更簡單的例子,可以使用srcset屬性控制尺寸;並且如果想同時使用WebP、JPEG格式圖片的話,可以使用<picture>元素綁定多個源。/<picture>


如果您對上述內容感到困惑,可以參考這篇指南,裡面對涉及的術語和示例給了很好的解釋。


無用的CSS:刪除導入


如果您正在使用Bootstrap之類的CSS框架,不要盲目地引入所有組件。實際上,一開始我會註釋掉所有不必要的組件,並在需要時再添加。下面是bootstrap.scss中的一小段內容:

Django性能指南:如何提升Django應用速度

我不會使用諸如badges和jumbotron之類的功能,所以將其註釋掉。


無用的CSS:使用PurgeCSS移除


一個更復雜的方法是使用類似PurgeCSS的庫,它可以分析您的文件,檢測CSS中無用的內容並刪除。PurgeCSS是一個NPM軟件包,因此如果您在Heroku上託管Django應用,需要安裝Node.js buildpack。


結論


我希望您至少已經找到一個可以優化Django應用程序的方案了。如果您有任何疑問、建議或反饋,請隨時在Twitter上給我留言。


附錄


用於QuerySet性能分析的裝飾器,如下是django_query_analyze裝飾器的代碼:

Django性能指南:如何提升Django應用速度


分享到:


相關文章: