Is it feasible to improve streaming HTML in Django?

Hello, Djangonauts!

I’ve been excited to leverage streaming HTML in Django since I read the impressive series of blog posts from Taylor Hunt, where he detailed how he vastly improved the experience of a Fortune 20 website, even on slow Android phones by using as little JavaScript as he could get away with and leveraging the old technology of streaming HTML.

I created a small proof of concept of streaming HTML in Django. It renders a home page of an e-commerce website that acts like it recommends specific items for the current viewer. I added an asyncio.sleep call to mimic a slow database query or API call. Thanks to the streaming HTML, all of the page before that section renders quickly, giving the impression of a performant website.

I’m excited that this PoC shows it’s possible, but I’m a little disappointed with how I got it to work. I had to:

  • render the shell, or base, HTML file that holds all the header content
  • split it apart at a place where I knew I could insert the content
  • create two additional templates to handle the parts of the page before and after the slow content
  • then render a fourth template for the slow data

An excerpt of the views.py file is below:

customized_recommendations = [
    # example data
    dict(name='Comfy Chair', discount_price=745.00, normal_price=800.00, review_count=1550, img='product1.jpg'),
    ...
]

async def recommendations():
    # split the shell template to sandwich the rest of the HTML
    pre_shell, post_shell = render_to_string('shell.html').split('<!-- footer -->')
    yield pre_shell
    yield render_to_string('home/home_pre.html')
    for item in customized_recommendations:
        await asyncio.sleep(.7)  # Faking an expensive database query or slow API
        yield render_to_string('home/_item.html', dict(recommendation=item))
    yield render_to_string('home/home_post.html')
    yield post_shell


async def index(request):
    return StreamingHttpResponse(recommendations())

Compare that to a similar implementation in Starlette / Jinja, where I could use one template:

async def homepage(request):
    recommendations = [
        dict(name='Comfy Chair', discount_price=745.00, normal_price=800.00, review_count=1550, img='product1.jpg'),
        ...
    ]

    async def slow_recommendations():
        # fake a slow query or API request
        for r in recommendations:
            delay = random.randint(0, 500) / 100
            await asyncio.sleep(delay)
            yield r

    template = templates.get_template('index.html')
    return StreamingResponse(
        content=template.generate_async(
            request=request,
            recommendations=slow_recommendations(),
        ),
        media_type='text/html',
    )

Calton asked me to look into what Jinja is doing with that generate_async method. I think it uses an async event loop to iterate through the templates, sending chunks down the wire when they’re ready and looping in a holding pattern while the async data call resolves.

I don’t know if improving the developer experience in this way is feasible, but I call it to your attention to see what you think.

Thank you so much for all your work at making such an incredible platform!

3 Likes

This has always been one of my “white whales” with async - where we can finally close the loop on rendering, as it were.

As it stands, I think we’d have to go in and overhaul the template engine to accomplish this; async functions, iterators and generators are different enough that they can’t really be slotted in, and the way templates render is especially difficult to work with in this context.

I’ve not been in that codebase in a while, but the first step would be having someone who knows templating somewhat well go and learn it and establish if there’s a core node render loop we can async-iterate over in a similar way to Jinja.

2 Likes

I wouldn’t panic, it’s hasn’t changed :stuck_out_tongue_winking_eye:

(Sorry for the noise, couldn’t resist. :slight_smile: )

1 Like

Just found this in my inbox. I think a good first step would be using Jinja’s streaming rendering working, and add any necessary general template functionality to make that easier to access. Then we could consider the much larger task of adding streaming rendering to Django’s template engine.

Hey Adam! I want to understand your thoughts.

Are you saying I should change the above proof of concept with the Jinja template engine?

Or are you identifying a strategy for moving forward with incorporating this into Django?

Thanks for either suggestion! :smiley:

1 Like

A strategy to incorporate this into Django. Since Django supports multiple template backends, we can divide the problem into general backend support and Django Template Language (DTL) engine support. First, let’s add whatever template backend support is necessary to get Jinja’s streaming rendering working. Then, let’s consider DTL support, which is going to be a much larger task.

hi, where are you with this new feature to add to django. is it available?

This came to mind when I was reviewing the list of proposed GSOC ideas: SummerOfCode2024 – Django

Adam do you think this is a good candidate to be added to the GSOC suggested ideas?

1 Like

I’ve found it surprisingly easy to incorporate Jinja streaming rendering working.

I updated the above proof of concept to incorporate Jinja’s template engine as a second engine and returned a streaming response:

from django.http import StreamingHttpResponse
from django.template.loader import get_template


async def index(request):
    template = get_template('home.html')
    return StreamingHttpResponse(template.template.generate_async(
      context=dict(recommendations=get_recommendations())
    ))
2 Likes

That’s nice @Chris-May ! Glad you got it working.

That code uses the template attribute of Django’s generic Template class to access the underlying Jinja template instance. It then calls the Jinja generate_async API.

The “general backend support” that I suggested would mean a public API in Django, on the generic Template class, like the existing Template.render. Perhaps it could be called Template.render_streaming(). It can fail for DTL for the time being.

On top, there could be StreamingTemplateResponse, django.shortcuts.render_streaming(), or whatever else might be useful.

@sarahboyce this first step is way too small for GSoC alone. Adding streaming rendering to DTL would require a bunch of experimentation but could work as a project for a dedicated student!

2 Likes

God I love this image so much