Caching i optymalizacja wydajności w aplikacjach webowych

Kacper Sieradziński
Kacper Sieradziński
28 kwietnia 2025Edukacja5 min czytania

Caching to jedna z najprostszych, a jednocześnie najskuteczniejszych technik zwiększania wydajności aplikacji webowych. Zamiast za każdym razem wykonywać kosztowne operacje — zapytania SQL, obliczenia czy wywołania do zewnętrznych API — możemy zachować wynik w pamięci i użyć go ponownie, skracając czas odpowiedzi z sekund do milisekund. W tym przewodniku poznasz różne poziomy cache'owania, jak skonfigurować Redis w Django, Flask i FastAPI, oraz jak wykorzystać CDN do przyspieszenia dostarczania plików statycznych.

Obraz główny Caching i optymalizacja wydajności w aplikacjach webowych

Dlaczego caching ma znaczenie?

Każda aplikacja ma swoje wąskie gardła: baza danych, zapytania HTTP, operacje na dużych zbiorach danych. Czasem 80% ruchu dotyczy tych samych danych, więc ponowne ich przetwarzanie to marnowanie zasobów. Cache pozwala przechować wynik kosztownej operacji i użyć go ponownie bez ponownego wykonywania tej operacji.

Korzyści z cache'owania

  • Drastyczne skrócenie czasu odpowiedzi — dane z pamięci są dostępne znacznie szybciej niż z bazy danych czy API.
  • Mniejsze obciążenie serwera i bazy danych — cache odciąża główne komponenty aplikacji.
  • Lepsze skalowanie aplikacji przy większym ruchu — cache pozwala obsłużyć więcej użytkowników bez zwiększania zasobów serwerowych.
  • Niższe koszty utrzymania infrastruktury — mniej zapytań do bazy oznacza mniejsze serwery i niższe rachunki.

Statystycznie, dobrze zaprojektowany cache może zwiększyć wydajność aplikacji nawet o 10-100x, szczególnie dla operacji odczytu danych.

Poziomy cache'owania

W praktyce stosuje się różne warstwy cache'u, każda odpowiedzialna za różne typy danych:

WarstwaCo przechowujePrzykład
Aplikacjadane wynikowe funkcji@lru_cache, Redis
Widok / endpointgotową odpowiedź HTTPcache middleware Django
Baza danychwynik zapytania SQLQuerySet cache
Frontend / CDNpliki statyczne, obrazyCloudflare, AWS CloudFront

Najlepsze efekty daje kombinacja kilku poziomów — np. Redis dla danych aplikacji + CDN dla plików statycznych. Każda warstwa cache'uje dane na różnym poziomie abstrakcji, co pozwala na maksymalną wydajność.

Cache na poziomie aplikacji

Cache na poziomie aplikacji przechowuje wyniki funkcji i metod. Jest to najbardziej elastyczna warstwa, pozwalająca cache'ować dowolne operacje.

Cache na poziomie widoku/endpointu

Cache na poziomie widoku przechowuje całą odpowiedź HTTP. To najszybszy sposób na cache'owanie, ale wymaga ostrożności — nie można cache'ować danych osobistych użytkownika.

Cache na poziomie bazy danych

Niektóre ORM oferują cache'owanie wyników zapytań, co pozwala uniknąć ponownych zapytań SQL dla tych samych danych.

Cache na poziomie CDN

CDN cache'uje pliki statyczne i odpowiedzi HTTP na serwerach edge na całym świecie, co przyspiesza dostarczanie treści do użytkowników.

Caching w Django

Django ma wbudowany system cache'owania i integrację z Redisem. Django oferuje kilka backends cache'owania: lokalna pamięć, baza danych, pliki, Memcached i Redis.

Konfiguracja Redis w Django

Python
1 2 3 4 5 6 7 8 9 10 11 12 # settings.py - konfiguracja cache Redis w Django CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, "KEY_PREFIX": "myapp", "TIMEOUT": 300,# Domyślny timeout w sekundach } }

Dla bardziej zaawansowanej konfiguracji możesz użyć django-redis:

Bash
1 pip install django-redis
Python
1 2 3 4 5 6 7 8 9 10 11 CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor", "PARSER": "django_redis.parsers.json.JSONParser", } } }

Cache'owanie widoku

Najprostszy sposób to użycie dekoratora cache_page:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_headers from django.shortcuts import render @cache_page(60 * 15)# cache na 15 minut def index(request): return render(request, 'index.html') # Cache z różnicowaniem według nagłówka @vary_on_headers('Accept-Language') @cache_page(60 * 15) def international_page(request): ...

Lub w urls.py:

Python
1 2 3 4 5 6 7 from django.views.decorators.cache import cache_page from django.urls import path from . import views urlpatterns = [ path('', cache_page(60 * 15)(views.index)), ]

Cache ręczny

Dla bardziej precyzyjnej kontroli możesz użyć ręcznego cache'owania:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from django.core.cache import cache from django.core.cache.utils import make_template_fragment_key def get_popular_posts(): cache_key = "popular_posts" data = cache.get(cache_key) if not data: # Kosztowna operacja data = list(Post.objects.order_by("-views")[:10].values()) cache.set(cache_key, data, 300)# Cache na 5 minut return data # Cache z wersjonowaniem def get_user_posts(user_id): cache_key = f"user_posts_{user_id}" data = cache.get(cache_key) if data is None:# Użyj 'is None' zamiast 'not data' dla list data = list(Post.objects.filter(author_id=user_id)) cache.set(cache_key, data, 600) return data # Inwalidacja cache po aktualizacji def update_post(post_id): post = Post.objects.get(id=post_id) post.save() # Wyczyść cache cache.delete(f"post_{post_id}") cache.delete("popular_posts")# Wyczyść też listę popularnych

Dzięki temu baza danych nie jest odpytana przy każdym wejściu na stronę. Cache'owanie ręczne daje pełną kontrolę nad tym, co i jak długo jest cache'owane.

Cache fragmentów szablonów

Django pozwala cache'ować fragmenty szablonów:

DJANGO
1 2 3 4 5 6 7 8 9 {% load cache %} {% cache 300 sidebar %} <div class="sidebar"> {% for post in popular_posts %} <a href="{{ post.get_absolute_url }}">{{ post.title }}</a> {% endfor %} </div> {% endcache %}

Cache per-user

Dla cache'owania danych specyficznych dla użytkownika:

Python
1 2 3 4 5 6 7 8 from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_cookie @vary_on_cookie @cache_page(60 * 5) def user_profile(request): # Cache będzie różny dla każdego użytkownika ...

Cache w Flask

Flask nie ma cache'owania w rdzeniu, ale integruje się z Flask-Caching, który oferuje pełną funkcjonalność cache'owania.

Instalacja

Bash
1 pip install Flask-Caching

Konfiguracja

Python
1 2 3 4 5 6 7 8 9 from flask import Flask from flask_caching import Cache app = Flask(__name__) app.config["CACHE_TYPE"] = "RedisCache" app.config["CACHE_REDIS_URL"] = "redis://localhost:6379/0" app.config["CACHE_DEFAULT_TIMEOUT"] = 300 cache = Cache(app)

Użycie dekoratora

Python
1 2 3 4 5 6 from datetime import datetime @app.route('/') @cache.cached(timeout=60) def index(): return f"Czas generowania: {datetime.now()}"

Za każdym wywołaniem w ciągu minuty Flask zwróci wynik z cache, bez ponownego przetwarzania.

Cache z parametrami

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @app.route('/user/<int:user_id>') @cache.cached(timeout=300, key_prefix='user') def get_user(user_id): user = User.query.get_or_404(user_id) return jsonify(user.to_dict()) # Ręczne cache'owanie @app.route('/expensive-operation') def expensive(): result = cache.get('expensive_result') if not result: result = perform_expensive_operation() cache.set('expensive_result', result, timeout=3600) return jsonify(result)

Cache memoize dla funkcji

Python
1 2 3 @cache.memoize(timeout=300) def get_user_posts(user_id): return Post.query.filter_by(author_id=user_id).all()

memoize automatycznie generuje klucz cache na podstawie argumentów funkcji.

Cache w FastAPI

FastAPI nie ma natywnego cache'a, ale łatwo zintegrować Redis z bibliotekami takimi jak fastapi-cache2 lub aiocache.

Instalacja fastapi-cache2

Bash
1 pip install fastapi-cache2 redis

Przykład użycia

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from fastapi import FastAPI from fastapi_cache import FastAPICache from fastapi_cache.backends.redis import RedisBackend from fastapi_cache.decorator import cache import redis.asyncio as redis app = FastAPI() @app.on_event("startup") async def startup(): redis_client = redis.from_url("redis://localhost") FastAPICache.init(RedisBackend(redis_client), prefix="fastapi-cache") @app.get("/users") @cache(expire=60) async def get_users(): # Kosztowna operacja - wykona się tylko raz na 60 sekund users = await fetch_users_from_db() return {"users": users} @app.get("/user/{user_id}") @cache(expire=300, key_builder=lambda *args, **kwargs: f"user:{kwargs['user_id']}") async def get_user(user_id: int): user = await fetch_user(user_id) return user

Dzięki temu każde wywołanie endpointu przez określony czas zwróci dane z cache.

Ręczne cache'owanie w FastAPI

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import redis.asyncio as redis redis_client = redis.from_url("redis://localhost") @app.get("/data") async def get_data(): cache_key = "expensive_data" # Sprawdź cache cached = await redis_client.get(cache_key) if cached: return json.loads(cached) # Jeśli nie ma w cache, wykonaj operację data = await expensive_operation() # Zapisz w cache await redis_client.setex( cache_key, 3600,# 1 godzina json.dumps(data) ) return data

Caching plików statycznych i CDN

Cache w backendzie to jedno, ale jeszcze ważniejszy jest cache w warstwie sieciowej — szczególnie dla plików statycznych. CDN (Content Delivery Network) rozkłada kopie plików (CSS, JS, obrazy) po całym świecie i automatycznie serwuje je z najbliższego serwera, odciążając Twój backend.

Najpopularniejsze CDN

  • Cloudflare — darmowy plan z dobrym wsparciem dla małych projektów
  • AWS CloudFront — integracja z resztą AWS
  • Bunny.net — szybki i tani
  • Google Cloud CDN — część ekosystemu GCP

Integracja CDN w Django

Python
1 2 3 4 5 6 # settings.py - konfiguracja CDN w Django STATIC_URL = 'https://cdn.example.com/static/' # lub dla AWS S3 STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' AWS_STORAGE_BUCKET_NAME = 'my-static-files' AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'

Konfiguracja cache headers w Nginx

NGINX
1 2 3 4 5 6 7 8 9 10 11 location /static/ { alias /app/staticfiles/; expires 30d; add_header Cache-Control "public, immutable"; } location /media/ { alias /app/media/; expires 7d; add_header Cache-Control "public"; }

Dzięki immutable przeglądarki wiedzą, że pliki statyczne nie zmieniają się i mogą cache'ować je bardzo długo.

Inne techniki optymalizacji

Cache to tylko część układanki. Aby Twoja aplikacja działała naprawdę szybko, zastosuj też:

1. Lazy loading danych

Unikaj wczytywania wszystkiego na raz — ładuj dane tylko wtedy, gdy są potrzebne:

Python
1 2 3 4 5 # Zamiast all_posts = Post.objects.all()# Ładuje wszystkie posty # Użyj paginacji posts = Post.objects.all()[:20]

Minimalizują liczbę zapytań SQL przez wstępne ładowanie powiązanych danych:

Python
1 2 3 4 5 6 7 8 9 10 11 12 # N+1 problem - wykonuje wiele zapytań posts = Post.objects.all() for post in posts: print(post.author.name)# Nowe zapytanie dla każdego posta # Rozwiązanie - select_related (dla ForeignKey) posts = Post.objects.select_related('author').all() for post in posts: print(post.author.name)# Brak dodatkowych zapytań # prefetch_related (dla ManyToMany i odwrotnych ForeignKey) posts = Post.objects.prefetch_related('tags', 'comments').all()

3. Asynchroniczne operacje I/O

W FastAPI wykorzystaj asynchroniczne operacje, aby nie blokować wątku podczas I/O:

Python
1 2 3 4 5 6 7 import httpx @app.get("/external-data") async def get_external_data(): async with httpx.AsyncClient() as client: response = await client.get("https://api.example.com/data") return response.json()

4. Kompresja odpowiedzi HTTP

Włącz kompresję gzip lub brotli w Nginx:

NGINX
1 2 3 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml; gzip_min_length 1000;

5. Profilowanie aplikacji

Użyj narzędzi do profilowania, aby znaleźć wąskie gardła:

Django:

Bash
1 pip install django-silk

Flask:

Bash
1 pip install flask-profiler

Uniwersalne:

Bash
1 2 pip install py-spy py-spy record -o profile.svg -- python app.py

6. Ustal limity cache TTL

Cache nie powinien żyć wiecznie — ustaw rozsądne czasy wygaśnięcia:

Python
1 2 3 4 5 # Krótki TTL dla dynamicznych danych cache.set('user_session', data, timeout=300)# 5 minut # Dłuższy TTL dla statycznych danych cache.set('site_config', config, timeout=86400)# 24 godziny

Typowe błędy w cache'owaniu

Przy implementowaniu cache'owania warto unikać następujących błędów:

1. Brak kluczy kontekstowych

Dane cache muszą być unikalne dla użytkownika / parametrów:

Python
1 2 3 4 5 6 # Źle - ten sam cache dla wszystkich data = cache.get('posts') # Dobrze - cache per user user_id = request.user.id data = cache.get(f'posts_user_{user_id}')

2. Cache niewłaściwych danych

Nie cache'uj danych wrażliwych ani danych, które zmieniają się zbyt często:

Python
1 2 3 4 5 # Źle - cache'owanie danych osobowych bez kontekstu użytkownika cache.set('all_users', sensitive_data) # Dobrze - cache'owanie publicznych, statycznych danych cache.set('public_posts', public_data)

3. Zbyt długi TTL

Przestarzałe dane mogą wprowadzać chaos:

Python
1 2 3 4 5 6 # Źle - cache na tydzień dla danych zmieniających się codziennie cache.set('daily_report', data, timeout=604800) # Dobrze - krótki TTL lub invalidation po zmianie cache.set('daily_report', data, timeout=3600) cache.delete('daily_report')# Po aktualizacji

4. Brak mechanizmu czyszczenia

Po aktualizacji danych wyczyść odpowiedni cache:

Python
1 2 3 4 5 6 def update_post(post): post.save() # Wyczyść wszystkie powiązane cache cache.delete(f'post_{post.id}') cache.delete('post_list') cache.delete('popular_posts')

5. Cache po stronie klienta bez HTTPS

Grozi manipulacją danych — zawsze używaj HTTPS w produkcji.

6. Cache stampede

Gdy cache wygasa jednocześnie dla wielu requestów, wszystkie próbują odświeżyć dane. Użyj lock'a:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import redis import time def get_data_with_lock(key): data = cache.get(key) if data: return data # Spróbuj uzyskać lock lock_key = f'{key}_lock' if cache.add(lock_key, 'locked', timeout=10): try: # Tylko jeden proces wykonuje kosztowną operację data = expensive_operation() cache.set(key, data, timeout=3600) finally: cache.delete(lock_key) else: # Poczekaj chwilę i spróbuj ponownie time.sleep(0.1) return cache.get(key) return data

Podsumowanie

Caching to jedno z tych rozwiązań, które potrafi zmienić wszystko — często bez zmiany kodu biznesowego. Dobrze zaprojektowany cache drastycznie zwiększa wydajność, redukuje koszty serwera, poprawia UX i umożliwia skalowanie aplikacji bez bólu.

W Pythonie masz pełen zestaw narzędzi — od wbudowanego cache w Django po Redis i CDN w chmurze. Najważniejsze jednak, by wiedzieć co, gdzie i na jak długo cache'ować. Pamiętaj, że cache to nie magia — wymaga przemyślanego podejścia i monitorowania, aby uniknąć problemów z przestarzałymi danymi.

Zacznij od prostego cache'owania widoków, stopniowo dodawaj cache na poziomie aplikacji dla kosztownych operacji, a następnie rozbuduj system o CDN dla plików statycznych. Monitoruj wydajność cache (hit ratio) i dostosowuj TTL w zależności od potrzeb aplikacji.