دليل لاختبار الأداء والتحسين باستخدام Python و Django
نشرت: 2022-03-11قال دونالد كنوث أن "التحسين المبكر هو أصل كل الشرور". ولكن يأتي وقت ، عادة في المشاريع الناضجة ذات الأحمال العالية ، عندما تكون هناك حاجة حتمية للتحسين. في هذه المقالة ، أود أن أتحدث عن خمس طرق شائعة لتحسين كود مشروع الويب الخاص بك. سأستخدم Django ، لكن المبادئ يجب أن تكون مماثلة لأطر العمل واللغات الأخرى. في هذه المقالة ، سأستخدم هذه الطرق لتقليل وقت استجابة استعلام من 77 إلى 3.7 ثانية.
رمز المثال مقتبس من مشروع حقيقي عملت معه ويظهر تقنيات تحسين الأداء. في حالة رغبتك في المتابعة ورؤية النتائج بنفسك ، يمكنك الحصول على الكود في حالته الأولية على GitHub وإجراء التغييرات المقابلة أثناء متابعة المقالة. سأستخدم Python 2 ، نظرًا لأن بعض حزم الجهات الخارجية غير متوفرة بعد ل Python 3.
تقديم تطبيقنا
يتتبع مشروع الويب الخاص بنا ببساطة العروض العقارية لكل بلد. لذلك ، يوجد نموذجان فقط:
# houses/models.py from utils.hash import Hasher class HashableModel(models.Model): """Provide a hash property for models.""" class Meta: abstract = True @property def hash(self): return Hasher.from_model(self) class Country(HashableModel): """Represent a country in which the house is positioned.""" name = models.CharField(max_length=30) def __unicode__(self): return self.name class House(HashableModel): """Represent a house with its characteristics.""" # Relations country = models.ForeignKey(Country, related_name='houses') # Attributes address = models.CharField(max_length=255) sq_meters = models.PositiveIntegerField() kitchen_sq_meters = models.PositiveSmallIntegerField() nr_bedrooms = models.PositiveSmallIntegerField() nr_bathrooms = models.PositiveSmallIntegerField() nr_floors = models.PositiveSmallIntegerField(default=1) year_built = models.PositiveIntegerField(null=True, blank=True) house_color_outside = models.CharField(max_length=20) distance_to_nearest_kindergarten = models.PositiveIntegerField(null=True, blank=True) distance_to_nearest_school = models.PositiveIntegerField(null=True, blank=True) distance_to_nearest_hospital = models.PositiveIntegerField(null=True, blank=True) has_cellar = models.BooleanField(default=False) has_pool = models.BooleanField(default=False) has_garage = models.BooleanField(default=False) price = models.PositiveIntegerField() def __unicode__(self): return '{} {}'.format(self.country, self.address)
يوفر نموذج HashableModel
المجرد أي نموذج يرث منه خاصية hash
تحتوي على المفتاح الأساسي للمثيل ونوع محتوى النموذج. يؤدي هذا إلى إخفاء البيانات الحساسة ، مثل معرفات المثيلات ، عن طريق استبدالها بعلامة تجزئة. قد يكون مفيدًا أيضًا في الحالات التي يكون فيها مشروعك به نماذج متعددة وتحتاج إلى مكان مركزي يفكك ويقرر ما يجب فعله مع مثيلات نماذج مختلفة من فئات مختلفة. لاحظ أنه بالنسبة لمشروعنا الصغير ، فإن التجزئة ليست ضرورية حقًا ، حيث يمكننا التعامل بدونها ، ولكنها ستساعد في إظهار بعض تقنيات التحسين ، لذلك سأحتفظ بها هناك.
ها هو فصل Hasher
:
# utils/hash.py import basehash class Hasher(object): @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): base36 = basehash.base36() content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): base36 = basehash.base36() unhashed = '%09d' % base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]
نظرًا لأننا نرغب في تقديم هذه البيانات من خلال نقطة نهاية واجهة برمجة التطبيقات ، فإننا نقوم بتثبيت إطار عمل Django REST وتحديد المسلسلات التالية وعرضها:
# houses/serializers.py class HouseSerializer(serializers.ModelSerializer): """Serialize a `houses.House` instance.""" id = serializers.ReadOnlyField(source="hash") country = serializers.ReadOnlyField(source="country.hash") class Meta: model = House fields = ( 'id', 'address', 'country', 'sq_meters', 'price' )
# houses/views.py class HouseListAPIView(ListAPIView): model = House serializer_class = HouseSerializer country = None def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country) return queryset def list(self, request, *args, **kwargs): # Skipping validation code for brevity country = self.request.GET.get("country") self.country = Hasher.to_object_pk(country) queryset = self.get_queryset() serializer = self.serializer_class(queryset, many=True) return Response(serializer.data)
الآن ، نقوم بملء قاعدة البيانات الخاصة بنا ببعض البيانات (ما مجموعه 100000 نسخة منزلية تم إنشاؤها باستخدام factory-boy
: 50000 لبلد واحد ، و 40000 لبلد آخر ، و 10000 لبلد ثالث) ونحن على استعداد لاختبار أداء تطبيقنا.
تحسين الأداء هو كل شيء عن القياس
هناك عدة أشياء يمكننا قياسها في المشروع:
- وقت التنفيذ
- عدد سطور الكود
- عدد مكالمات الوظيفة
- الذاكرة المخصصة
- إلخ.
ولكن ليست جميعها ذات صلة بقياس مدى جودة أداء مشروعنا. بشكل عام ، هناك مقياسان رئيسيان هما الأكثر أهمية: طول مدة تنفيذ شيء ما ومقدار الذاكرة التي يحتاجها.
في مشروع الويب ، عادةً ما يكون وقت الاستجابة (الوقت المطلوب للخادم لتلقي طلب تم إنشاؤه بواسطة إجراء بعض المستخدمين ومعالجته وإرسال النتيجة مرة أخرى) هو المقياس الأكثر أهمية ، لأنه لا يسمح للمستخدمين بالملل أثناء الانتظار للرد والتبديل إلى علامة تبويب أخرى في المتصفح.
في البرمجة ، يسمى تحليل أداء المشروع التنميط. من أجل تحديد أداء نقطة نهاية API الخاصة بنا ، سوف نستخدم حزمة الحرير. بعد تثبيته وإجراء /api/v1/houses/?country=5T22RI
(التجزئة التي تتوافق مع البلد مع إدخال 50000 منزل) ، نحصل على هذا:
200 احصل
/ api / v1 / منازل /
77292ms بشكل عام
15854ms على الاستفسارات
50004 استفسار
يبلغ إجمالي وقت الاستجابة 77 ثانية ، يتم إنفاق 16 ثانية منها على الاستفسارات في قاعدة البيانات ، حيث تم إجراء ما مجموعه 50000 استفسار. بوجود هذه الأعداد الضخمة ، هناك مجال كبير للتحسين ، فلنبدأ.
1. تحسين استعلامات قاعدة البيانات
إحدى النصائح الأكثر شيوعًا حول تحسين الأداء هي التأكد من تحسين استعلامات قاعدة البيانات. هذه الحالة ليست استثناء. علاوة على ذلك ، يمكننا القيام بالعديد من الأشياء بخصوص استفساراتنا لتحسين وقت الاستجابة.
1.1 توفير جميع البيانات دفعة واحدة
بإلقاء نظرة فاحصة على ما هو 50000 استعلام ، يمكنك أن ترى أن هذه كلها طلبات بحث زائدة عن الحاجة إلى جدول " houses_country
":
200 احصل
/ api / v1 / منازل /
77292ms بشكل عام
15854ms على الاستفسارات
50004 استفسار
في | الجداول | ينضم | وقت التنفيذ (مللي ثانية) |
---|---|---|---|
+0: 01: 15.874374 | "منازل_بلد" | 0 | 0.176 |
+0: 01: 15.873304 | "منازل_بلد" | 0 | 0.218 |
+0: 01: 15.872225 | "منازل_بلد" | 0 | 0.218 |
+0: 01: 15.871155 | "منازل_بلد" | 0 | 0.198 |
+0: 01: 15.870099 | "منازل_بلد" | 0 | 0.173 |
+0: 01: 15.869050 | "منازل_بلد" | 0 | 0.197 |
+0: 01: 15.867877 | "منازل_بلد" | 0 | 0.221 |
+0: 01: 15.866807 | "منازل_بلد" | 0 | 0.203 |
+0: 01: 15.865646 | "منازل_بلد" | 0 | 0.211 |
+0: 01: 15.864562 | "منازل_بلد" | 0 | 0.209 |
+0: 01: 15.863511 | "منازل_بلد" | 0 | 0.181 |
+0: 01: 15.862435 | "منازل_بلد" | 0 | 0.228 |
+0: 01: 15.861413 | "منازل_بلد" | 0 | 0.174 |
مصدر هذه المشكلة هو حقيقة أن مجموعات الاستعلام في Django كسولة . هذا يعني أن مجموعة الاستعلام لا يتم تقييمها ولا تصل إلى قاعدة البيانات حتى تحتاج فعليًا إلى الحصول على البيانات. في الوقت نفسه ، تحصل فقط على البيانات التي أخبرتها بها ، وتقدم طلبات لاحقة إذا كانت هناك حاجة إلى أي بيانات إضافية.
هذا بالضبط ما حدث في حالتنا. عند تعيين الاستعلام من خلال House.objects.filter(country=country)
، تحصل Django على قائمة بجميع المنازل في البلد المحدد. ومع ذلك ، عند إجراء تسلسل لمثيل house
، يتطلب HouseSerializer
مثيل country
الخاص بالمنزل لحساب حقل country
الخاص بالمسلسل. نظرًا لأن بيانات الدولة غير موجودة في مجموعة الاستعلام ، تقدم django طلبًا إضافيًا للحصول على تلك البيانات. وهو يفعل ذلك لكل منزل في مجموعة الاستعلام — أي 50000 مرة في المجموع.
ومع ذلك ، فإن الحل بسيط للغاية. لاستخراج جميع البيانات المطلوبة للتسلسل ، يمكنك استخدام select_related()
في مجموعة الاستعلام. وبالتالي ، get_queryset
بنا كما يلي:
def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country).select_related('country') return queryset
دعونا نرى كيف أثر ذلك على الأداء:
200 احصل
/ api / v1 / منازل /
35979ms بشكل عام
102 مللي ثانية على الاستفسارات
4 استفسارات
انخفض وقت الاستجابة الإجمالي إلى 36 ثانية والوقت المستغرق في قاعدة البيانات حوالي 100 مللي ثانية مع 4 استفسارات فقط! هذه أخبار رائعة ، لكن يمكننا فعل المزيد.
1.2 تقديم البيانات ذات الصلة فقط
بشكل افتراضي ، يستخرج Django جميع الحقول من قاعدة البيانات. ومع ذلك ، عندما يكون لديك جداول ضخمة بها العديد من الأعمدة والصفوف ، فمن المنطقي أن تخبر Django بالحقول المحددة التي يجب استخراجها ، حتى لا يقضي الوقت في الحصول على معلومات لن تُستخدم على الإطلاق. في حالتنا ، نحتاج فقط إلى خمسة حقول للتسلسل ، لكن لدينا 17 حقلاً. من المنطقي تحديد الحقول المراد استخلاصها من قاعدة البيانات بالضبط ، حتى نخفض وقت الاستجابة بشكل أكبر.
لدى Django طريقتا defer()
only()
لمجموعة الاستعلام للقيام بذلك بالضبط. الأول يحدد الحقول التي لا يجب تحميلها والثاني يحدد الحقول التي سيتم تحميلها فقط .
def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country)\ .select_related('country')\ .only('id', 'address', 'country', 'sq_meters', 'price') return queryset
أدى هذا إلى تقليل الوقت المستغرق في الاستعلامات إلى النصف ، وهو أمر جيد ، ولكن 50 مللي ثانية ليست بهذا القدر. انخفض الوقت الإجمالي أيضًا بشكل طفيف ، ولكن هناك مساحة أكبر لقطعه.

200 احصل
/ api / v1 / منازل /
33111 مللي ثانية بشكل عام
52 مللي ثانية على الاستفسارات
4 استفسارات
2. تحسين التعليمات البرمجية الخاصة بك
لا يمكنك تحسين استعلامات قاعدة البيانات بشكل غير محدود ، وقد أظهرت نتائجنا الأخيرة ذلك للتو. حتى لو قللنا افتراضيًا الوقت الذي نقضيه في الاستفسارات إلى 0 ، فسنظل نواجه حقيقة الانتظار لمدة نصف دقيقة للحصول على الرد. حان الوقت للانتقال إلى مستوى آخر من التحسين: منطق الأعمال .
2.1 تبسيط التعليمات البرمجية الخاصة بك
في بعض الأحيان ، تأتي حزم الجهات الخارجية مع الكثير من النفقات العامة للمهام البسيطة. أحد الأمثلة على ذلك هو مهمتنا في إعادة أمثلة المنزل المتسلسلة.
يعد برنامج Django REST Framework رائعًا ، مع الكثير من الميزات المفيدة خارج الصندوق. ومع ذلك ، فإن هدفنا الرئيسي الآن هو تقليل وقت الاستجابة ، لذلك فهو مرشح رائع للتحسين ، خاصة وأن العناصر المتسلسلة بسيطة للغاية.
دعونا نكتب متسلسل مخصص لهذا الغرض. لتبسيط الأمر ، سيكون لدينا طريقة ثابتة واحدة تؤدي المهمة. في الواقع ، قد ترغب في الحصول على نفس توقيعات الفئة والطريقة لتتمكن من استخدام المتسلسلات بالتبادل:
# houses/serializers.py class HousePlainSerializer(object): """ Serializes a House queryset consisting of dicts with the following keys: 'id', 'address', 'country', 'sq_meters', 'price'. """ @staticmethod def serialize_data(queryset): """ Return a list of hashed objects from the given queryset. """ return [ { 'id': Hasher.from_pk_and_class(entry['id'], House), 'address': entry['address'], 'country': Hasher.from_pk_and_class(entry['country'], Country), 'sq_meters': entry['sq_meters'], 'price': entry['price'] } for entry in queryset ] # houses/views.py class HouseListAPIView(ListAPIView): model = House serializer_class = HouseSerializer plain_serializer_class = HousePlainSerializer # <-- added custom serializer country = None def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country) return queryset def list(self, request, *args, **kwargs): # Skipping validation code for brevity country = self.request.GET.get("country") self.country = Hasher.to_object_pk(country) queryset = self.get_queryset() data = self.plain_serializer_class.serialize_data(queryset) # <-- serialize return Response(data)
200 احصل
/ api / v1 / منازل /
17312ms بشكل عام
38 مللي ثانية على الاستفسارات
4 استفسارات
هذا يبدو أفضل الآن. انخفض وقت الاستجابة إلى النصف تقريبًا نظرًا لحقيقة أننا لم نستخدم رمز DRF المتسلسل.
نتيجة أخرى قابلة للقياس - عدد مكالمات الوظائف الإجمالية التي تم إجراؤها أثناء دورة الطلب / الاستجابة - انخفض من 15،859،427 مكالمة (من الطلب المقدم في القسم 1.2 أعلاه) إلى 9،257،469 مكالمة. هذا يعني أن حوالي 1/3 من جميع استدعاءات الوظائف تم إجراؤها بواسطة Django REST Framework.
2.2 تحديث / استبدال حزم الطرف الثالث
تعد تقنيات التحسين الموضحة أعلاه هي الأكثر شيوعًا ، وهي تلك التي يمكنك القيام بها بدون تحليل وتفكير شاملين. ومع ذلك ، لا تزال 17 ثانية تبدو طويلة جدًا ؛ لتقليل هذا الرقم ، سنحتاج إلى التعمق في الكود الخاص بنا وتحليل ما يحدث تحت الغطاء. بعبارة أخرى ، سنحتاج إلى تشكيل الكود الخاص بنا.
يمكنك إجراء التنميط بنفسك ، باستخدام ملف تعريف Python المدمج ، أو يمكنك استخدام بعض حزم الجهات الخارجية لذلك (التي تستخدم ملف تعريف Python المدمج). نظرًا لأننا نستخدم silk
بالفعل ، فيمكنه تحديد الكود وإنشاء ملف ملف تعريف ثنائي ، يمكننا تصوره بشكل أكبر. هناك العديد من حزم التصور التي تحول ملف تعريف ثنائي إلى بعض التصورات الثاقبة. سأستخدم حزمة snakeviz
.
فيما يلي تصور للملف الشخصي الثنائي للطلب الأخير من أعلى ، مرتبطًا بطريقة إرسال العرض:
من أعلى إلى أسفل هو مكدس الاستدعاءات ، ويعرض اسم الملف ، واسم الطريقة / الوظيفة مع رقم السطر والوقت التراكمي المقابل الذي يقضيه في تلك الطريقة. من السهل الآن رؤية أن حصة الأسد من الوقت مخصصة لحساب التجزئة (المستطيلات __init__.py
و primes.py
من اللون البنفسجي).
حاليًا ، هذا هو عنق الزجاجة الرئيسي في الأداء في الكود الخاص بنا ، لكنه في الوقت نفسه ليس رمزنا - إنه حزمة طرف ثالث.
في مثل هذه الحالات ، هناك عدد محدود من الأشياء التي يمكننا القيام بها:
- تحقق من وجود إصدار جديد من الحزمة (نأمل أن يكون له أداء أفضل).
- ابحث عن حزمة أخرى تؤدي أداءً أفضل في المهام التي نحتاجها.
- اكتب تطبيقنا الخاص الذي سيتفوق على أداء الحزمة التي نستخدمها حاليًا.
لحسن الحظ بالنسبة لي ، هناك إصدار أحدث من basehash
المسؤولة عن التجزئة. يستخدم الرمز v.2.1.0 ، ولكن هناك v.3.0.4. مثل هذه المواقف ، عندما تكون قادرًا على التحديث إلى إصدار أحدث من الحزمة ، تكون أكثر احتمالًا عندما تعمل في مشروع موجود.
عند التحقق من ملاحظات الإصدار للإصدار 3 ، توجد هذه الجملة المحددة التي تبدو واعدة جدًا:
تم إجراء إصلاح شامل باستخدام خوارزميات البدائية. بما في ذلك (كذا) دعم gmpy2 إذا كان متاحًا (كذا) على النظام من أجل زيادة أكبر بكثير.
دعنا نكتشف ذلك!
pip install -U basehash gmpy2
200 احصل
/ api / v1 / منازل /
7738 مللي ثانية بشكل عام
59ms على الاستفسارات
4 استفسارات
قللنا وقت الاستجابة من 17 إلى أقل من 8 ثوانٍ. نتيجة رائعة ، ولكن هناك شيء آخر يجب أن ننظر إليه.
2.3 إعادة بناء الكود الخاص بك
حتى الآن ، قمنا بتحسين استفساراتنا ، واستبدلنا الشفرة المعقدة والعامة لطرف ثالث بوظائفنا المحددة للغاية ، وحزم الطرف الثالث المحدثة ، لكننا تركنا الكود الحالي كما هو. لكن في بعض الأحيان ، يمكن أن تؤدي عملية إعادة هيكلة صغيرة للشفرة الحالية إلى نتائج مبهرة. لكن لهذا نحتاج مرة أخرى إلى تحليل نتائج التنميط.
عند إلقاء نظرة فاحصة ، يمكنك أن ترى أن التجزئة لا تزال تمثل مشكلة (وليس من المستغرب أنها الشيء الوحيد الذي نفعله ببياناتنا) ، على الرغم من أننا تحسننا في هذا الاتجاه. ومع ذلك ، فإن المستطيل الأخضر الذي يقول أن __init__.py
يستهلك 2.14 ثانية يزعجني ، جنبًا إلى جنب مع __init__.py:54(hash)
الرمادي الذي يليه مباشرة. هذا يعني أن بعض التهيئة تستغرق وقتًا طويلاً.
دعنا نلقي نظرة على الكود basehash
للحزمة الأساسية.
# basehash/__init__.py # Initialization of `base36` class initializes the parent, `base` class. class base36(base): def __init__(self, length=HASH_LENGTH, generator=GENERATOR): super(base36, self).__init__(BASE36, length, generator) class base(object): def __init__(self, alphabet, length=HASH_LENGTH, generator=GENERATOR): if len(set(alphabet)) != len(alphabet): raise ValueError('Supplied alphabet cannot contain duplicates.') self.alphabet = tuple(alphabet) self.base = len(alphabet) self.length = length self.generator = generator self.maximum = self.base ** self.length - 1 self.prime = next_prime(int((self.maximum + 1) * self.generator)) # `next_prime` call on each initialized instance
كما ترى ، فإن تهيئة المثيل base
تتطلب استدعاء دالة next_prime
؛ هذا ثقيل جدًا كما نرى في المستطيلات اليسرى السفلية من التصور أعلاه.
دعنا نلقي نظرة على صفي Hash
مرة أخرى:
class Hasher(object): @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): base36 = basehash.base36() # <-- initializing on each method call content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): base36 = basehash.base36() # <-- initializing on each method call unhashed = '%09d' % base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]
كما ترى ، لقد قمت بتسمية طريقتين تقومان بتهيئة مثيل base36
على كل استدعاء طريقة ، وهو أمر غير مطلوب حقًا.
نظرًا لأن التجزئة هي إجراء حتمي ، مما يعني أنه بالنسبة لقيمة إدخال معينة يجب أن تولد دائمًا نفس قيمة التجزئة ، فيمكننا جعلها سمة فئة دون الخوف من كسر شيء ما. دعنا نتحقق من كيفية أدائها:
class Hasher(object): base36 = basehash.base36() # <-- initialize hasher only once @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return cls.base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): unhashed = '%09d' % cls.base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]
200 احصل
/ api / v1 / منازل /
إجمالي 3766 مللي ثانية
38 مللي ثانية على الاستفسارات
4 استفسارات
النتيجة النهائية أقل من أربع ثوان ، وهي أصغر بكثير مما بدأنا به. يمكن تحقيق مزيد من التحسين لوقت الاستجابة باستخدام التخزين المؤقت ، لكنني لن أعالجها في هذه المقالة.
خاتمة
تحسين الأداء هو عملية تحليل واكتشاف. لا توجد قواعد صارمة تنطبق على جميع الحالات ، حيث أن لكل مشروع تدفقه واختناقاته الخاصة. ومع ذلك ، فإن أول شيء يجب عليك فعله هو إنشاء ملف تعريف للكود الخاص بك. وإذا كان بإمكاني في مثل هذا المثال القصير تقليل وقت الاستجابة من 77 ثانية إلى 3.7 ثانية ، فإن المشاريع الضخمة لديها إمكانات تحسين أكبر.
إذا كنت مهتمًا بقراءة المزيد من المقالات ذات الصلة بـ Django ، فراجع أفضل 10 أخطاء ارتكبها مطورو Django من قبل زميل Toptal Django Developer Alexandr Shurigin.