---
title: "Julia Evans is rediscovering why frameworks exist: remove a hundred small frictions"
locale: "en"
url: "https://irz.fr/en/articles/django-julia-evans-small-frictions-en"
markdown_url: "https://irz.fr/en/articles/django-julia-evans-small-frictions-en.md"
category: "tech"
tags: ["Django", "Python", "framework", "web", "SQLite"]
published_at: "2026-08-24T09:52:00.000Z"
author: "Léa Perrin"
translation: "https://irz.fr/fr/articles/django-julia-evans-small-frictions-fr.md"
---

# Julia Evans is rediscovering why frameworks exist: remove a hundred small frictions

QuerySets, template filters and migrations show Julia Evans why Django removes repetitive work, until one forgotten cache setting exposes the price of hidden conventions.

Julia Evans is building what she calls a “2010-style” website: an SQL database, HTML rendered on the server and very little JavaScript.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/) Her comfortable toolkit before this was rather different — static site generators, static pages with small JavaScript experiments, or simple Vue SPAs connected to Lambda or a Go backend. That arrangement worked well while an application was basically one page; once she wanted many different pages, the frontend-heavy options became less attractive.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

She is trying to recover on the server what she liked about those small SPAs, namely keeping most of the logic in one place. Django becomes useful for a fairly mundane reason. Evans already knows SQL and backend programming, so she is not waiting for a framework to reveal a new capability; she is discovering how comfortable it is when somebody else has already settled a hundred recurring little jobs.

## Small savings

In her July 21, 2026 post, Evans points to modest things — custom `QuerySet`s, template filters, the `querystring` tag and automatic migrations — that would hardly justify changing stacks one by one, yet together remove a long tail of operations that otherwise get rewritten, renamed from page to page or simply forgotten.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

> **A framework wins by accumulation**
> - migrations already created in Evans' project: 19
> - light load test before restoring template caching: 2–3 req/s
> - light test after restoring the cache: ≈12 req/s
> The performance figures are Evans' observations on a roughly $10/month VM; she explicitly says this was not a careful before-and-after benchmark.

A few lines from her view show the result. Evans defines an `EventQuerySet` with `approved()`, `for_tab()`, `with_festivals()`, `is_free()` and `is_outdoors()`; when she builds the page, those names make the selections visible immediately, without forcing a reader to decode the underlying columns and operators each time.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

Django explicitly supports this pattern. A custom `QuerySet` can define its own methods and expose them through a manager; the documentation includes `as_manager()` and `Manager.from_queryset()` as ways to make that API available from the model.[2](https://docs.djangoproject.com/en/6.0/topics/db/managers/)

Evans says her earlier instinct was essentially that she already knew SQL, so why bother with a query builder.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/) The project changes her mind about a narrower point: **local vocabulary** such as `approved()` states the intention immediately, whereas `approved_at__isnull=False` makes each reader reconstruct it.

## Readable query

The difference looks cosmetic while a query fits on one line, then starts organizing the program once a page combines several optional filters.

> **One query, two levels**
> - Repeat conditions, column names and optional cases directly inside each view.: Mechanics
> - Events.objects.approved().for_tab(...).is_free(...).is_outdoors(...).: Intent
> SQL has not disappeared. Repeated query decisions have moved into vocabulary shared by the application.

A Django `QuerySet` remains lazy, which means it can be built, filtered and passed around before the database is actually touched; evaluation comes later.[2](https://docs.djangoproject.com/en/6.0/topics/db/managers/) Chaining these helpers does not fire one query per method. Evans dislikes parts of the filter-definition syntax, then spends most of her time on the nicer side of the API: reading the named methods inside ordinary view code.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

Her example separates two questions that ORM arguments often collapse together — whether the syntax is prettier than SQL, and where repeated filtering decisions should live — with the interesting answer mostly belonging to the second.

## Ordinary HTML

The same pattern appears in templates through `urlize`, `linebreaksbr`, date formatting and `json_script`. Turning text URLs into links, preserving line breaks, formatting a date or inserting JSON safely is almost comically small work, which is exactly why teams so often end up scattering their own versions of it through a project.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

Their expense comes from attention rather than difficulty: each job is too small to deserve a subsystem and common enough to appear repeatedly, so Django keeps many of them in the standard toolbox of its template engine.[3](https://docs.djangoproject.com/en/6.0/ref/templates/builtins/)

Evans particularly likes `querystring`. Her site uses parameters such as `?date=2026-06-01` or `?outdoors=...` to represent filters, and the tag can build a link to **the same query string with one value changed**, or remove a parameter by passing `None`.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

> **A filter without plumbing**
> - The page stores filter state in the URL: date, free, outdoors…: 1
> - The template keeps existing parameters.: 2
> - querystring changes only the targeted value.: 3
> - The resulting link remains shareable without a second frontend state store.: 4
> The saving is tiny per link and meaningful when a whole filtering interface follows the convention.

To be sure, the behaviour still exists somewhere; here it lives in the framework, sparing the application another local helper whose author must check encoding, preservation of unrelated filters and removal of empty parameters.

## Nineteen migrations

The same kind of convenience appears below the visible interface, where the project has already accumulated **19 migrations** and Evans expects more.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/) She does not describe that count as debt; it records how often the schema has been allowed to follow the project while her picture of the problem became clearer.

Django treats migrations as a versioned history of database schema. `makemigrations` compares current models with the state reconstructed from previous migrations and writes new declarative operations; `migrate` applies those operations to a database.[4](https://docs.djangoproject.com/en/6.0/topics/migrations/) Django's own documentation still recommends reading the generated output because complex changes are not perfectly autodetected.[4](https://docs.djangoproject.com/en/6.0/topics/migrations/)

> **What 19 migrations mean**
> - Changing the model immediately means designing a schema transformation and making it reproducible elsewhere.: Without history
> - A model change produces a versioned artifact that travels with the code and can be replayed.: With migrations
> Automation does not eliminate difficult migrations; it makes ordinary schema changes much cheaper.

QuerySets, templates and migrations all tell the same story: Django industrializes **the ordinary case**. Difficult migrations still exist, while an ordinary field addition or schema adjustment no longer begins with a procedure invented specifically for this project.

## Wrong setting

The reverse side appears when so much useful behaviour arrives preinstalled that part of the system becomes invisible, leaving developers to recognize when they have accidentally switched one of those conventions off.

Evans runs into that problem after LLM-related scrapers begin sending roughly ten requests per second; on a VM costing about $10 per month, a light ApacheBench test then shows the site serving only **2–3 requests per second**.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

A CPU profile shows a surprising amount of time spent rendering templates. Django's performance documentation notes that the cached template loader can materially improve performance by avoiding template compilation on every render.[5](https://docs.djangoproject.com/en/2.0/topics/performance/) In Evans' setup the cache was supposed to be enabled by default, but she had accidentally disabled it while changing something else.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

After restoring template caching, she observes **around 12 requests per second** without exhausting the CPU.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/) Evans explicitly warns that this was no rigorous before-and-after benchmark, so the anecdote proves no universal fourfold speed-up; what it does expose is how **an invisible convention can perform substantial work until one setting quietly removes it**.

> **The hidden contract**
> - The cached loader avoids compiling templates on every render.: Useful default
> - Evans accidentally disables it while changing settings.: Configuration
> - Template rendering appears as a large expense.: CPU profile
> - Returning to the expected behaviour changes her load test substantially.: Correction
> The more conventions a framework provides, the more understanding its settings becomes a skill of its own.

## Not everything

The post also spends time on conventions Evans dislikes, which keeps it well away from a “ten reasons to use Django” list.

She tries using inheritance to share logic across four class-based views, finds the result unpleasant to follow and returns to straightforward functions.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/) Inheriting from a Django interface such as `QuerySet` bothers her much less, which makes the preference narrower than an anti-class doctrine: the friction appears when **her own application logic** has to live inside an inheritance hierarchy.

Questions about capacity remain open: whether the site should scale for bursts, whether more work deserves caching, whether Jinja is worth trying, and how much the documented difference between blocks and includes matters in practice. Evans lists them without pretending to have selected a final architecture.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

That uncertainty makes the field report more useful than advocacy, because a framework never removes decisions altogether; it chooses which recurring decisions deserve an already paved path.

## One stack

Evans explains that her previous small frontend applications worked well when almost all logic could stay in the browser. For a site with many different pages, she is now chasing the same quality in the other direction: server-rendered HTML, little JavaScript and logic concentrated on the backend.[1](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)

Seen through this project, Django looks less like one monolithic framework than a set of continuities between database, URL, view and HTML: migrations carry schema history, QuerySets carry domain vocabulary, templates already manipulate dates and query strings, and shared defaults cover escaping as well as template loading.

In practice, **complexity mostly changes address**. Evans writes less local plumbing and inherits a broader set of shared conventions, which then need to be familiar enough that an unexpected setting stands out.

Her enthusiasm comes from a developer who already knows how to build these pieces deciding that rebuilding them has stopped being interesting. Go, Flask or a handful of helpers could reproduce the individual behaviours; when the same small jobs recur across thousands of sites, however, letting a framework encounter them first starts to look perfectly rational.

## References

1. [Julia Evans, Some more things about Django I've been enjoying, July 21, 2026](https://jvns.ca/blog/2026/07/21/more-nice-django-things/)
2. [Django 6.0 — Managers and custom QuerySets](https://docs.djangoproject.com/en/6.0/topics/db/managers/)
3. [Django 6.0 — Built-in template tags and filters](https://docs.djangoproject.com/en/6.0/ref/templates/builtins/)
4. [Django 6.0 — Migrations](https://docs.djangoproject.com/en/6.0/topics/migrations/)
5. [Django — Performance and optimization](https://docs.djangoproject.com/en/2.0/topics/performance/)
