Week 11 · Performance: measuring and caching
The one-minute story
A tea stall at a railway station. Every customer orders "one chai". If the stall-owner boils fresh water, adds tea leaves and waits four minutes each time, the queue grows and customers walk away. So the owner makes a big pot in the morning and pours from it. That pot is a cache: a ready answer to a question that gets asked again and again.
But the pot gets cold (the answer becomes stale), so after an hour the owner throws it away and brews again (expiry, or TTL). If the price of sugar changes, he must brew a new pot at once (invalidation).
Before you buy a bigger stove, you measure: how long does one chai take, how many chai per hour, which step is slow? Maybe the real problem is a customer who keeps asking for 300 cups one at a time (the N+1 problem), or a slow tap (a missing index). Performance work is: measure, find the bottleneck, fix that one thing, measure again.
By the end of this week you can:
- name the numbers that describe speed (latency, throughput, percentiles, TTFB, page load) and the tools that measure them;
- find a slow spot: N+1 queries, a missing index, a heavy page;
- use browser caching headers, Flask-Caching with Redis,
@cache.cachedand@cache.memoize, and predict response times in exam code; - design cache keys, choose an expiry and decide when to invalidate;
- speed up a Vue front end (lazy loading, compression, debounce) and add pagination;
- run a before and after measurement.
Words you need
- Latency - the time one request takes, from sending to receiving the answer.
- Throughput - how many requests per second the server finishes.
- Percentile (p50, p95, p99) - p95 = 95 percent of requests were faster than this number. It shows the slow tail that an average hides.
- TTFB (time to first byte) - time until the first byte of the response arrives.
- FCP / TTI / TBT / CLS - Lighthouse metrics: First Contentful Paint, Time to Interactive, Total Blocking Time, Cumulative Layout Shift.
- Profiling - measuring which part of the code takes the time.
- N+1 queries - one query for a list plus one extra query for each item.
- Index - a lookup structure that lets the database find rows without scanning the whole table.
- Cache - stored copies of answers, reused instead of recomputing.
- Hit / miss / hit ratio - found in cache / not found / hits divided by all lookups.
- TTL (time to live) - seconds after which a cache entry expires.
- Invalidation - removing or refreshing a cache entry when its data changed.
- Memoisation - caching a function's return value by its arguments.
- CDN - Content Delivery Network: servers around the world that deliver your static files from a place near the user.
- Debounce - wait until the user stops typing before acting.
Idea 1: What to measure, and with what
"The site is slow" is not a measurement. Pick numbers.
- Latency / response time: one request, in milliseconds.
- Throughput: requests per second under load.
- Percentiles: report p50 (median), p95, p99. An average of 100 ms can hide a p99 of 3 seconds.
- TTFB: how long the server (plus network) took to start answering.
- Page load: the whole page, shaped mostly by the number of HTTP requests and the size of files (images, JavaScript).
Lighthouse Performance score is built from FCP, TTI (Time to Interactive), TBT, Speed Index and layout shift (CLS). It does not include "database query time" or "HTTPS usage".
| Tool | What it does | Typical use |
|---|---|---|
| Browser dev tools, Network tab | every request, size, time, headers, cache status | which file is big or slow? |
| Dev tools, Performance tab | flame chart of scripting, rendering | why is the page janky? |
| Lighthouse (in Chrome dev tools) | scores and advice; can emulate throttled network and mobile devices | whole-page audit |
Apache Bench ab | fires many requests at one URL | simple throughput/latency test |
| Locust | Python-scripted virtual users | realistic load with logins |
time / time.perf_counter() | stopwatch for a command or code | one function |
Flask debug toolbar, cProfile | which Python function is slow | profiling |
Apache Bench example (100 requests, 10 at a time):
ab -n 100 -c 10 http://127.0.0.1:5000/api/books
Read Requests per second (throughput), Time per request (latency) and the Percentage of the requests served within a certain time table (percentiles).
A tiny stopwatch in Python:
import time
def timed(fn, *a):
t = time.perf_counter()
result = fn(*a)
return result, round((time.perf_counter() - t) * 1000, 1) # milliseconds
(a) "Most users wait under 400 ms but a few wait 5 s." Which metric exposes this? (b) "The server handled 800 requests in 10 seconds." What is the throughput? (c) Which Lighthouse item is not a performance metric: FCP, TTI, TBT, DQT?
Need a hint?
Percentiles; divide; one of them is invented.
Show the full solution
(a) p95 or p99 (the tail), not the average. (b) 800 / 10 = 80 requests per second. (c) DQT (Database Query Time) is not a Lighthouse metric.
Idea 2: Finding the bottleneck
Do not guess. Measure the layers in order: network (dev tools), server (profiling, logs), database (query count and time), front end (scripts).
N+1 queries
You load 3 authors, then for each author you touch author.books. That is 1 query for the list plus 3 more: N+1. With 1000 authors it is 1001 queries.
from sqlalchemy import select
from sqlalchemy.orm import selectinload
# slow: lazy loading, one query per author
for a in db.session.scalars(select(Author)):
print(a.name, len(a.books))
# fast: load all books for all authors in one extra query
stmt = select(Author).options(selectinload(Author.books))
for a in db.session.scalars(stmt):
print(a.name, len(a.books))
I counted the SQL statements with SQLAlchemy's before_cursor_execute event on 3 authors with 2 books each: the first loop ran 4 queries, the second only 2. A related fix is a SQL JOIN.
Missing indexes
Without an index the database reads every row (a "full table scan") to find WHERE author_id = 7. An index on that column lets it jump straight to the rows.
class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey("author.id"), index=True)
title = db.Column(db.String(100))
In SQLite you can ask for the plan: EXPLAIN QUERY PLAN SELECT * FROM book WHERE author_id = 7; shows SEARCH ... USING INDEX when an index is used and SCAN when it is not. Indexes speed up reads but slow down writes a little and take space, so index the columns you filter or join on, not everything.
Other usual suspects
- Returning too much data (a full table instead of one page): use pagination.
- Heavy work in the request: sending mail, generating a PDF, a CSV of 100000 rows. Move it to a Celery task (week 10).
- Big images and scripts on the front end.
- Repeating the same expensive answer for every user: cache it.
- Optimising before measuring. The slow part is often not the one you suspect.
- Judging by the average. Look at p95 and p99.
- Testing on your fast laptop with a tiny database. Use realistic data volume.
Idea 3: Cache layers
A request may be answered at several layers. The earlier, the cheaper.
| Layer | Where | Example | Shared by users? |
|---|---|---|---|
| Browser cache | the user's browser (a private cache) | Cache-Control: max-age | no, one per client |
| CDN / shared cache / reverse proxy | servers between user and you | CDN serving your JS and images | yes |
| Application cache | memory or Redis, near your Flask code | Flask-Caching | yes |
| Database cache | the database's own buffers or a stored result | query cache, a summary table | yes |
A CDN is a group of servers that delivers web content fast, usually from a server near the client (usually, not always). A shared cache may be used for many users, so it must not hold personalised responses. A private cache is tied to one client. A reverse proxy (such as Nginx) can cache responses for similar later requests. A forward proxy protects clients, not servers, so it is a different thing.
The main benefits of caching: lower latency, lower load on the origin server, less bandwidth, better handling of traffic spikes (scalability). It does not give encryption, DDoS protection or remove the need for the database altogether.
Browser caching headers
The server tells the browser how to cache with response headers.
| Header | Meaning |
|---|---|
Cache-Control: max-age=3600 | reuse for 3600 seconds without asking |
Cache-Control: no-store | do not store at all (sensitive data) |
Cache-Control: no-cache | may store, but revalidate before each reuse |
Cache-Control: public / private | shared caches may / may not store it |
Expires | an older absolute date for expiry |
ETag | a fingerprint of the content |
Last-Modified | when the content last changed |
Revalidation: the browser sends If-None-Match: <etag> (or If-Modified-Since). If nothing changed, the server replies 304 Not Modified with an empty body, and the browser reuses its copy. Cheap, but still one round trip.
from flask import Flask, make_response, request, jsonify
app = Flask(__name__)
@app.get("/data")
def data():
r = make_response(jsonify(x=1))
r.headers["Cache-Control"] = "public, max-age=60"
r.add_etag()
return r.make_conditional(request)
I tested this with the Flask test client: the first request returned 200 with an ETag; repeating it with If-None-Match: <that etag> returned 304 and an empty body.
Related facts that appear in exams: localStorage has more space than cookies and keeps data after the browser closes; sessionStorage is cleared when the tab or browser closes. A hard refresh bypasses the cache for the page's files but does not delete the whole browser cache of the site. Browsers can cache JavaScript files, and normally only GET requests (without a body) are cached.
Which of these control caching: Cache-Control, Expires, Bearer, Content-Type, ETag? What status code means "your copy is still valid"?
Need a hint?
One of them is about login, one about format.
Show the full solution
Cache-Control, Expires and ETag (validation). Bearer belongs to Authorization (tokens) and Content-Type states the data format. The status is 304 Not Modified.
Idea 4: Application caching with Flask-Caching and Redis
pip install flask-caching redis
import time
from flask import Flask
from flask_caching import Cache
app = Flask(__name__)
app.config.update(
CACHE_TYPE="RedisCache", # or "SimpleCache" (in-process memory) for tests
CACHE_REDIS_URL="redis://localhost:6379/1",
CACHE_DEFAULT_TIMEOUT=300, # seconds, applies when a decorator gives no timeout
)
cache = Cache(app)
SimpleCache keeps entries in the Python process (lost on restart, not shared between processes), good for learning. RedisCache stores them in Redis: shared between workers, survives an app restart.
@cache.cached caches the whole view
@app.get("/time")
@cache.cached(timeout=5)
def get_time():
return str(time.time())
The first request runs the function and stores the response. Requests within 5 seconds get the stored copy: the function is skipped. After 5 seconds the entry has expired and the function runs again.
Question (like a real End Term question). CACHE_DEFAULT_TIMEOUT = 5 and @cache.cached() with no argument on get_time, which returns the current time. Request 1 at 00:00:00, request 2 at 00:00:02, request 3 at 00:00:06. What does each return?
Solution.
- Request 1: miss, runs, returns
00:00:00and stores it (expires at 00:00:05). - Request 2: 2 s later, still fresh: hit, returns
00:00:00. - Request 3: 6 s later, expired: miss, runs again, returns
00:00:06.
Answer: 00:00:00, 00:00:00, 00:00:06. I checked the same logic with SimpleCache and a 2 second timeout: two quick calls matched, a call after the timeout differed.
The key matters: cached vs memoize
@cache.cached(timeout=60) caches a view. By default its key is the URL path (request.path). Two different paths are two different entries. But if you pass key_prefix='compute', the key is that fixed string, so different URLs share one entry!
@cache.memoize(timeout=60) caches a function's return value and builds the key from the function and its arguments. get_data("a") and get_data("b") are separate entries; get_data("a") twice is one computation. This is memoisation.
calls = []
@cache.memoize(timeout=60)
def square(n):
calls.append(n)
return n * n
square(3); square(3); square(4)
print(calls) # [3, 4] (the second square(3) was a hit)
cache.delete_memoized(square, 3) # invalidate only square(3)
square(3)
print(calls) # [3, 4, 3]
I ran exactly this and got [3, 4] and then [3, 4, 3].
Question.
@app.route('/compute/<int:x>/<int:y>')
@cache.cached(timeout=60, key_prefix='compute')
def compute(x, y):
return jsonify({'result': x * y})
/compute/10/20 is requested, then /compute/5/10 within 60 seconds. What are the two responses?
Solution. The key is the fixed string compute, not the URL. The first call stores {"result": 200} under that key. The second URL finds the same key and gets {"result": 200} again, although 5 times 10 is 50. So the answers are 200 and 200. Without key_prefix (default key from the path) they would be 200 and 50.
Question.
@cache.memoize(timeout=300)
def compute_data(user_id):
time.sleep(5)
return f"Data for user {user_id}"
Requests for user ids 1, 2, 1 arrive within 5 minutes. Total time?
Solution. id 1: miss (5 s). id 2: a different argument, miss (5 s). id 1 again: hit (about 0 s). Total 10 seconds.
Variation: a view @cache.cached(timeout=40) that sleeps 30 s, visited by user 1, and by user 2 three minutes later: the entry (40 s) has expired, so user 2 also waits 30 s, and the difference in response times is about 0 seconds. With timeout=120 also 3 minutes later, again expired. And "two requests within 50 seconds to a view cached for 100 seconds with a 25 s sleep": the first takes 25 s, the second about 0, so the difference is 25 seconds. Always compare the gap between requests with the timeout.
A more careful look at how long entries live
- The
timeoutin the decorator overridesCACHE_DEFAULT_TIMEOUT. - A memoize timeout of 180 s with visits at 0, 150 s and 210 s: the first is a miss, the second a hit, the third is only 60 s after the second but 210 s after the store, so it is a miss. (TTL counts from when the entry was stored, not from the last visit.)
- Expiry is a time to live and does not refresh on a hit.
- "
@cache.cachedcaches by function arguments." No: it caches a view by its path (or bykey_prefix);memoizeuses the arguments. The statement that "memoize does not include parameters in the key" is false. - "Caching removes the need for a database." No, only for repeated lookups.
- A cached personalised page (a page showing "Hello Asha") returned to a second user is a bug and a privacy leak. Put the user id in the key, or do not cache it.
- TTL runs from the store time.
- Time-based questions: draw a timeline, mark the expiry, and then see which requests land before it.
Idea 5: Cache keys, expiry and invalidation
Key design
A good key is unique for every different answer and identical for every repeat of the same answer.
| Answer depends on | Key should include |
|---|---|
| nothing about the user (a list of public books) | books:page:2 |
| one user | user:7:dashboard |
| a filter | books:genre:sci:page:2 |
| a score per country | score:country:India |
Bad keys: too coarse (all users share one entry), or including something that changes every time (a timestamp), which gives a hit ratio of zero.
Expiry (TTL)
Pick it by how stale the data may be. A dashboard that can be 5 minutes old: TTL 300. A social feed or stock price: short TTL. Caching a feed for a long time is a bad idea (the question that calls it "recommended" is false). Rarely changing data (list of countries): long TTL.
Invalidation strategies
| Strategy | How | Good | Bad |
|---|---|---|---|
| Time-based | just let the TTL expire | simplest | stale until expiry |
| Delete on write | when data changes, cache.delete(key) (or delete_memoized) | always fresh next read | you must remember every write path |
| Write-through | on write, update the database and the cache | reads stay fast and fresh | more code, two stores to keep consistent |
@app.post("/books")
def add_book():
# ... save to the database ...
cache.delete("view//books") # or cache.delete_memoized(list_books)
return {"ok": True}, 201
To find the key of a cached view: with the default settings it is view/ plus the path, for example view//books (the double slash is correct, as the path starts with /). Easier and safer: use cache.delete_memoized(function) for memoised functions, or give views an explicit key_prefix.
Hit ratio and stampede (concept)
Hit ratio = hits / (hits + misses). 90 hits and 10 misses is 0.9. A low ratio means bad keys or a TTL that is too short.
Cache stampede: a popular entry expires and 500 requests arrive at the same moment. All see a miss and all hit the database together. Remedies at concept level: one request recomputes while the others wait (a lock), refresh a little before expiry, add a small random amount to each TTL so entries do not all expire together, or warm the cache from a background job.
GET /scores/countries/<country_name> returns the total of all players' scores for a country. (a) Where can you cache it? (b) Give a key. (c) When must it be invalidated?
Need a hint?
There is more than one layer. Think about what changes the answer.
Show the full solution
(a) On the backend (Redis) and also in the browser with a short Cache-Control: max-age. Both help. (b) score:country:<country_name>, for example score:country:India. (c) Whenever a score is added or changed for a player of that country: delete that key on write (or accept slightly stale data with a short TTL, such as 60 s).
@cache.cached(timeout=100)
def home():
time.sleep(30)
return "hello"
User 1 visits. User 2 visits 3 minutes later. What is the difference in their response times? Same question if user 2 visited after 50 seconds.
Need a hint?
Compare 180 s and 50 s with the timeout.
Show the full solution
After 180 s the entry expired (timeout 100), so user 2 also waits 30 s: difference about 0. After 50 s it is a hit: user 2 waits about 0, user 1 waited 30, difference 30 seconds.
Examiners ask "what do you cache and what happens when data changes?" Have a one-sentence answer for each cached item, for example: "dashboard summary, key user id, 5 minutes TTL, deleted when the user adds a task". Also demonstrate it: open dev tools or print a timestamp to show the second call is fast. A cache with no expiry and no invalidation will be questioned.
Idea 6: Front-end performance and pagination
Most of the delay a user sees is in the browser: many requests, big files, and heavy scripts.
| Technique | What it does |
|---|---|
| Lazy loading routes / components | load a page's JavaScript only when the user visits it |
| Code splitting | the build tool breaks the app into chunks; the initial download is smaller |
| Minification | strips spaces and shortens names in JS/CSS |
| Compression (gzip, brotli) | the server sends the file compressed; the browser unpacks it |
| Image optimisation | right size, modern format (WebP), loading="lazy" on images |
| Fewer requests | bundle small files, use a CDN for libraries |
| Debounce | wait for a pause in typing before calling the API |
Lazy-loaded route in Vue 2 + Vue Router 3 (this needs a build setup such as Vue CLI; it will not work from a CDN script tag):
const routes = [
{ path: "/", component: Home },
{ path: "/reports", component: () => import("./views/Reports.vue") } // downloaded on first visit
];
Debounce, so typing "abc" in a search box makes one API call, not three. I ran this in Node:
function debounce(fn, ms) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
}
const search = debounce((q) => console.log("search", q), 100);
search("a"); search("ab"); search("abc"); // quick typing
setTimeout(() => search("abcd"), 300); // later
It printed search abc and then search abcd. The first three calls collapsed into the last one.
Pagination
Never send 50000 rows. Send a page at a time.
@app.get("/api/books")
def books():
page = request.args.get("page", 1, type=int)
p = db.paginate(select(Book).order_by(Book.id), page=page, per_page=20, error_out=False)
return jsonify(items=[b.title for b in p.items], page=p.page, pages=p.pages, has_next=p.has_next)
With 6 books and per_page=4, page 2 holds 2 items, pages is 2, has_next is False (I ran it). Always add an order_by so pages are stable.
Async jobs for heavy work
Anything slower than a second or two (PDF, CSV export, sending mail to everyone) goes to a Celery task: the API replies at once with a task id, and the page polls the status or is told when it finishes (see week 10).
Task. The endpoint /api/authors is slow. Improve it and prove it.
- Measure before:
ab -n 200 -c 10 http://127.0.0.1:5000/api/authors. Write down requests per second and the 95 percent time. Suppose you get 12 requests/s, p95 = 900 ms. - Find the cause: count queries (the N+1 listener from Idea 2). 1 + 50 queries per request: N+1.
- Fix 1:
selectinload(Author.books). Measure again: for example 40 requests/s, p95 = 250 ms. - Fix 2: add
@cache.cached(timeout=60). Measure again: for example 600 requests/s, p95 = 20 ms. - Check correctness: add an author, confirm the list updates (delete on write or wait 60 s).
Record a small table of before, after 1, after 2 in your report. (The numbers above are only an illustration; your machine gives its own.) One change at a time, so you know what helped.
A Vue app loads one 3 MB bundle and a search box calls the API on every key press. List three fixes.
Need a hint?
Lazy loading, compression, debounce.
Show the full solution
(1) Lazy-load routes with () => import(...) so the first download is small. (2) Turn on minification and gzip (and optimise images). (3) Debounce the search input (for example 300 ms). Optional: paginate the search results.
Exam radar
The evidence file has about 42 distinct matching questions. Keyword counts: about 134 of 1471 End Term, 12 of 608 Quiz 2 and 6 of 848 Quiz 1 questions match (rough; several matches are really Vue Router questions from neighbouring weeks, and weeks 10 to 12 have few matched questions overall). Honest reading: this is an End Term topic, and the pattern is very stable.
What the real questions test:
- Predict the time with Flask-Caching (about 10 questions). Code with
@cache.cached(timeout=...)or@cache.memoize(timeout=...), atime.sleep(N), and two or three visits at given times. You choose "0 seconds", "N seconds" or "difference N". Also thetime.time()version with request timestamps. @cache.cachedvs@cache.memoize: view vs function arguments; memoisation definition.- Caching in general (about 8): benefit (lower server load, lower latency, scalability), shared vs private cache, CDN, what to cache or not, hard refresh, "a browser cannot cache JavaScript" (false).
- HTTP headers (about 3):
Cache-Control,Expires,ETag,no-store; and the "which statement is false" withsessionStorage. - Lighthouse (about 3): FCP, TTI, Layout Shift count; "Database Query Time" is not one; it can emulate throttling and devices.
- Page load factors: number of HTTP requests and file sizes.
- Redis as a cache store; Celery for async work and CSV or reports (week 12 evidence).
- Compare the gap between requests with the timeout, not with the sleep time.
key_prefixmakes different URLs share an entry.- Different arguments to a memoised function are different entries.
no-storeprevents caching;no-cachemeans "revalidate".sessionStoragedoes not survive closing the browser.- Caching does not add security.
- A shared cache must not store personalised responses.
Quick quiz
@cache.cached(timeout=100) wraps a view that sleeps 25 seconds. A client requests the same URL twice within 50 seconds. The approximate difference between the two latencies is...
- 0 seconds
- 25 seconds
- 50 seconds
- 100 seconds
A memoised function with time.sleep(5) is called for ids 1, 2, 1 within its timeout. Total time?
- 5 seconds
- 10 seconds
- 15 seconds
- 0 seconds
Select all TRUE statements. (select all that apply)
@cache.memoizebuilds the key from the function arguments.- A CDN is a group of servers that delivers content quickly.
sessionStoragedata stays after the browser is closed and reopened.- A shared cache is suitable for personalised responses.
sessionStorage is cleared when the session ends; shared caches must not hold personal data.Which is NOT a Lighthouse performance metric?
- First Contentful Paint
- Time to Interactive
- Total Blocking Time
- Database Query Time
With @cache.cached(timeout=60, key_prefix='compute') on /compute/<x>/<y>, what do /compute/10/20 then /compute/5/10 return (multiplication)?
- 200 then 50
- 200 then 200
- 50 then 200
- 100 then 100
Which fixes an N+1 problem?
- Adding
@cache.cachedto a Pythonprint - Loading related rows with
selectinloador a join - Using
no-store - Increasing the timeout
Viva: say it out loud
How did you measure your app's performance?
I use the browser Network tab for request sizes and times, Lighthouse for the page, and Apache Bench or Locust for load. I look at latency percentiles and throughput, not only the average. I measured before and after each change, one change at a time.
What did you cache and how do you keep it fresh?
I cache expensive read endpoints, such as the dashboard summary, with Flask-Caching on Redis. The key includes the user id, the TTL is a few minutes, and I delete the key when that user writes data. So readers get fast answers and writers see their changes.
Difference between cache.cached and cache.memoize?
cached caches a whole view and by default uses the URL path as the key. memoize caches a function's return value, with the key built from the function and its arguments, so different arguments get different entries.
What is a cache stampede?
When a popular entry expires, many requests miss together and all hit the database at once. We reduce it with a lock so only one request recomputes, with random extra TTL, or by refreshing before expiry.
How do browser caching headers work?
Cache-Control: max-age lets the browser reuse a file without asking. ETag or Last-Modified let it ask "has this changed?" and the server answers 304 with no body if not. no-store stops caching of sensitive responses.
Name three things you did to make the front end faster.
I lazy-load the heavier routes, serve minified and compressed files, optimise images, debounce the search box, and paginate long lists so the browser never receives thousands of rows.
Why is a heavy task like CSV export done in Celery?
It can take tens of seconds and would block the request. The API starts a task and returns at once, and the page polls the task status, then downloads the file.
Recap
- Measure first: latency, throughput, p95/p99, TTFB. Tools: dev tools, Lighthouse,
ab, Locust, profilers. - Common bottlenecks: N+1 queries, missing indexes, too much data, heavy work inside requests, big front-end files.
- Cache layers: browser (private), CDN and proxies (shared), application (Redis), database.
@cache.cached= view, key by path (orkey_prefix);@cache.memoize= function, key by arguments. TTL counts from the store time.- Keep caches fresh with TTL, delete on write or write-through; watch the hit ratio and stampedes.
- Front end: lazy routes, code splitting, minify, gzip, images, debounce. Server: pagination, Celery for heavy jobs.
ab -n 100 -c 10 URL(requests, concurrency)- Hit ratio = hits / (hits + misses); throughput = requests / seconds
Cache-Control: max-age=60,no-store,no-cache,public,private;ETagplusIf-None-Matchgives304- Config:
CACHE_TYPE="RedisCache",CACHE_REDIS_URL="redis://localhost:6379/1",CACHE_DEFAULT_TIMEOUT=300 @cache.cached(timeout=60);@cache.cached(timeout=60, key_prefix="k");@cache.memoize(timeout=60)cache.delete("key"),cache.delete_memoized(fn, *args),cache.clear()- N+1 fix:
select(Author).options(selectinload(Author.books)) - Index:
db.Column(db.Integer, index=True) - Pagination:
db.paginate(select(Book).order_by(Book.id), page=2, per_page=20) - Lazy route:
component: () => import("./views/X.vue") - Next: week 12, the project; earlier: week 10
- MAD1 week 4 - Models: databases, SQL, ER diagrams and SQLAlchemy: Indexes and query design are the first performance lever; caching is the second.
- MAD1 week 7 - Backend systems: indexing, search, caching and logging: Caching and indexing from MAD1 are measured and tuned in MAD2's performance week.
Practise with real exam questions
These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week All End Term papers