We learn as we go, we write as we learn.

About michelada.io


By joining forces with your team, we can help with your Ruby on Rails and Javascript work. Or, if it works better for you, we can even become your team! From e-Commerce to Fintech, for years we have helped turning our clients’ great ideas into successful business.

Go to our website for more info.

Tags


Django + Turbolinks

6th October 2020

Introduction

In Mexico, michelada.io is known for working hard to get really great products using Ruby and Ruby on Rails,
although we work on different technologies such as JavaScript and Python.

This post looks at how to speed up a Django web application using Turbolinks. Also, we will cover how to configure
a Vanilla JS environment to get the better performance from templates and preload of cached views.

Context

When we work with Server Side Render applications, we need to handle the response of our requests in different ways. Usually, the faster the response is, the better the User Experience will be but sometimes, this may be hard to achieve when you have to do some data computing or complex calculations before you send the response.

In Django applications, we follow a common architecture pattern named MTV (Model - Template - Views). The Django core team is pushing the community to put the business logic inside the Model as much as you can, even though you use it to fetch, create and update our models in the database. Templates are used for just the presentation layer with HTML, CSS and JS and Views are in charge of handling the request life cycle; The application server will receive a request by a user asking for a resource, then the view, based on some logic will ask the Model, if is needed, for that in order to send that information as context for rendering a HTML file.

django-mtv

In Ruby on Rails projects, it is common to use Turbolinks to improve the performance of a web application. If you do not know why, here's a reason:

Turbolinks makes navigating your web application faster. Get the performance benefits of a single-page application without the added complexity of a client-side JavaScript framework. Use HTML to render your views on the server side and link to pages as usual. When you follow a link, Turbolinks automatically fetches the page, swaps in its , and merges its , all without incurring the cost of a full page load.
  • Turbolinks team

In short, turbolinks will use HTML5 History API to do a pre fetch of content for all the URLs inside an anchor element before presenting it.

When you work with RoR, turbolinks is pretty straightforward to enable. On the other hand, for Django projects, we need to do it manually. Let’s start with that.

Django project

All the code for this project will be hosted in this repository and you will be able to see every step checking out the branches.

If you have experience with Django and Python, I recommend you to skip until the Turbolinks section.

Setup

Every Python project needs to start with a new virtual environment. For that, Python3, since it’s version 3.3, has a module named venv. As we know, Python is a programming language with batteries included.

python3 -m venv .venv

Then, we need to activate it:

source .venv/bin/activate

With our virtualenv activated, we need to install some of our dependencies:

pip install django          # The web framework for perfectionists with deadlines.
pip install python-decouple # Package to keep our secrets into a `.env` file.
pip install dj-database-url # Package to build our database connection given a url.
pip install psycopg2        # Package to use as postgres client.

Now, let’s start our project

django-admin startproject blog .

Then, we need to create our .env file at the root directory with next content. Make sure you add your own data.

DEBUG=True # False for
DATABASE_URL=postgres://your_db_user:your_db_user_psw@your_db_host:your_db_host_port/your_db_name
SECRET_KEY=YOUR_STRONG_SECRET_KEY

We can start a new app module where we are going to store all our bussiness logic of that.

python manage.py startapp articles

Let's modify our Django blog/settings.py file. We will start importing python-decouple and dj-database-url and allow our setting file to take some values from our .env file.

...
import dj_database_url
from decouple import config

...

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)

...

DATABASES = {
    'default': dj_database_url.config(
        default=config('DATABASE_URL')
    )
}

...

You can check the complete file in the step-01 branch in the repository.

Creating our model

We can start working with our model so, go to articles/models.py and add the following content:

class Article(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    published_at = models.DateTimeField(auto_now_add=True)

Make sure to run the migrations flow:

python manage.py makemigrations
python manage.py migrate

Now in Django shell, you will be able to start adding objects to the database. I strongly recommend you to install faker with pip: we'll use it to populate our database for testing purposes. Also, if you checkout to this branch, you will see how to implement faker in a Django command with this command:

python manage.py articles_faker --number=300

The result will be 300 records in our database of our Article model. Feel free to check step-02 branch.

Using class-based view

Django has class-based views to allow re-using as much logic as you want by implementing inheritance with classes that the framework already has. Open the file articles/views.py and put the next content:

from django.views.generic.list import ListView
from .models import Article

class ArticlesView(ListView):
    model = Article
    paginate_by = 7
    context_object_name = 'articles'
    template_name = 'articles/index.html'
    ordering = ['-published_at']

Add a new file named articles/urls.py with this content:

from django.urls import path
from .views import ArticlesView

urlpatterns = [
    path('', ArticlesView.as_view(), name='articles')
]

As specified in our ArticlesView, we need to create a file templates/articles/index.html where we can either add the next content or implement what it is in the step-03 branch:

<table>
{% for a in articles %}
  <tr>
     <td>{{a.title}}</td>
  </tr>
{% endfor %}
</table>

{% if page_obj.has_previous %}
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Prev</a>
{% endif %}

Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}

{% if page_obj.has_next %}
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}

Do not forget to include our new URLs file into our project base URL file at blog/urls.py:

urlpatterns = [
    ...
    path('', include('articles.urls')),
    ...
]

Remember that you can see step-03 for reference.

First of all, Turbolinks is a JS library and the team behind has allowed a download of the standalone version through a CDN like this. Download it and put into static/js/turbolinks.js

Now we shall start importing this as a static file into our base template of our project. Do not forget to do it inside the <head> section. Also, our HTML file should have some special tags to allow turbolinks to work with it:

...
<html lang="en"  data-controller="dmc-turbolinks">

<head>
    ...
    <meta name="turbolinks-root" content="/">
    <meta name="turbolinks-cache-control" content="no-cache">
    ...
    <script type="text/javascript" src="{% static 'js/turbolinks.js' %}"></script>
    ...
</head>
...

Do not forget we are working with a JS file so, we need to initialize it. Create a new document file in static/js/application.js and use this code to initialize and then, import it into our base HTML template.

# static/js/application.js
'use strict'

document.addEventListener("turbolinks:load", function () {
  // We will live our vanilla js code handler
})


# template/application.html

<script type="module" src="{% static 'js/application.js' %}" data-turbolinks-eval="false"></script>

I recommend you to take a look at the step-04 branch in case you have doubts.

How do we know it works?

In order to test if our turbolinks implementation works, we will edit our ArticlesView class by overrding a method to add a Time.sleep(). Open the articles/views.py file to override the get_context_data method of our class.

import random
import time
...

class ArticlesView(ListView):
    ...
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        time.sleep(random.randint(2, 8)) # <-- HERE 
        return context

This change allow us to see the classic Turbolinks progress bar. Now we know our site is doing a pre-fetching with turbolinks. We can improve the performance to allow Turbolinks use HTTP History API.

So far, we have covered all until our step-05 branch in our repository.

demo-02

In order to give more powers to our turbolinks implementation, we need to override the Location header when the request comes from turbolinks. Let's create a new file in utils/turbolinks.py with this:

class TurbolinksMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)

        is_turbolinks = request.META.get('HTTP_TURBOLINKS_REFERRER')
        is_response_redirect = response.has_header('Location')

        if is_turbolinks:
            if is_response_redirect:
                location = response['Location']
                prev_location = request.session.pop('_turbolinks_redirect_to', None)
                if prev_location is not None:
                    # relative subsequent redirect
                    if location.startswith('.'):
                        location = prev_location.split('?')[0] + location
                request.session['_turbolinks_redirect_to'] = location
            else:
                if request.session.get('_turbolinks_redirect_to'):
                    location = request.session.pop('_turbolinks_redirect_to')
                    response['Turbolinks-Location'] = location
        return response

The code above is an implementation of the popular django-material package. If you want, you can see more about it here at the repository.

Also I recommend you check our repository in the step-06 branch.

Conclusion

Django is known for being a web framework with batteries included in the same way as Python however this article covered how to include a new battery in our project in order to increase the performance of our project. If you are interest in more content like this, tell us in Twitter or, if you want some help with a Django project, you can send an email to info@michelada.io.

View Comments