Page Layout Caching

Good day, All,

With my use of the Dash app, I encountered an issue where I wanted the page layouts to be cached clientside. This is different from caching on the server side, as there is no need to allow the request to event leave the client’s computer.

I wanted this because I have semi-dynamic layouts that are costly to run, but dont need to be run every time the user loads the page. In fact, these layouts hardly need to change except for when I make a coding change.

Thus, I took a stab at being able to cache the layout from the callback that the _pages_location does onto the _pages_content (page_container).

This is what I came up with, use this example app to test it out. :slight_smile:


app.py

import dash_ag_grid as dag
from dash import Dash, html, dcc, page_container, register_page, page_registry, Input, Output
import plotly.express as px
import dash_bootstrap_components as dbc
import time
import random

app = Dash(__name__, use_pages=True, pages_folder='', external_stylesheets=[dbc.themes.BOOTSTRAP])

df = px.data.gapminder()
continents = df['continent'].unique().tolist()

layout1 = html.Div([dcc.Graph(figure=px.bar(df, x='year', y='pop'))])

def layout2():
    time.sleep(3) #emulate long call
    dff = df[df['continent'] == continents[random.randint(0, len(continents)-1)]]
    return html.Div([dcc.Graph(figure=px.bar(dff, x='year', y='pop')),
                    dag.AgGrid(rowData=dff.to_dict('records'), columnDefs=[{'field': i} for i in dff.columns])])

register_page('layout1_cache', path='/', layout=layout1)
register_page('layout1_nocache', path='/1_nocache', layout=layout1)

## notice how this layout stays static even when the layout should be dynamic
register_page('layout2_cache', path='/2_cache', layout=layout2) 
register_page('layout2_nocache', path='/2_nocache', layout=layout2)

app.layout = html.Div(
    [
        dbc.NavbarSimple([
            dbc.NavItem(dbc.NavLink(href=pg['path'], children=pg['title'])) for pg in page_registry.values()
        ] + [dbc.Button('Destroy Cache', id='destroy', n_clicks=0)],
                         brand="Caching Demo", brand_href='#', color="dark", dark=True),
        dcc.Loading(
            page_container
        )
    ],
    id='testing',
)

app.clientside_callback(
    """async function(id) {
        EXCLUDED_PAGES = ['/1_nocache', '/2_nocache']
        await preloadPages(['/', '/2_cache'])
        return window.dash_clientside.no_update
    }""", Output('testing', 'id'), Input('testing', 'id')
)

app.clientside_callback(
    """async function(n) {
        await caches.delete(CACHE)
        alert('you have deleted your cache')
        return window.dash_clientside.no_update
    }""", Output('destroy', 'id'), Input('destroy', 'n_clicks'), prevent_initial_call=True
)

if __name__ == "__main__":
    app.run(debug=True)

assets/custom.js

const { fetch: originalFetch } = window;

const stored_data = {};

const CACHE = "PAGE_CACHE";

function updateCache(request, response) {
    if (window.location.href.substr(0,4).toLowerCase() == 'https' || window.location.href.includes('127.0.0.1')) {
        caches.open(CACHE).then(function (cache) {
        cache.put(request, response);
        });
    } else {
        stored_data[request] = response
    }
}

async function testCache(url, that, args, cache_loc = CACHE) {
    var cachedResponse;
    if (window.location.href.substr(0,4).toLowerCase() == 'https' || window.location.href.includes('127.0.0.1')) {
        const cache = await caches.open(cache_loc)
        cachedResponse = await cache.match(url)
    } else {
        cachedResponse = stored_data[url]
    }

    // Return a cached response if we have one
    if (cachedResponse) {
        savedResponse = cachedResponse.clone()
        return savedResponse;
    }

    const result = originalFetch(that, args);

    result.then((response) => {
        if (response.ok) {
            updateCache(url, response.clone())
        }
    })
    return result
}

var EXCLUDED_PAGES = [];

window.fetch = async (event, payload) => {
    var result;
    // store all pages
    if (event == '/_dash-update-component') {
        data = payload.body
        if (data.includes('_pages_store')) {
            loc = JSON.parse(data)['inputs'][0].value.toLowerCase()
            if (!EXCLUDED_PAGES.includes(loc)) {
                result = await testCache(loc, event, payload)
            }
        }
    }

    if (!result) {
        result = originalFetch(event, payload)
    }

    return result
}

async function fetchHandler (url) {
    var cachedResponse;
    if (window.location.href.substr(0,4).toLowerCase() == 'https' || window.location.href.includes('127.0.0.1')) {
        const cache = await caches.open(CACHE)
        cachedResponse = await cache.match(url)
    } else {
        cachedResponse = stored_data[url]
    }

    if (!cachedResponse) {
        payload = {"output":".._pages_content.children..._pages_store.data..",
        "outputs":[{"id":"_pages_content","property":"children"},
        {"id":"_pages_store","property":"data"}],
        "inputs":[{"id":"_pages_location","property":"pathname","value": url.toLowerCase()},
        {"id":"_pages_location","property":"search","value":""}],
        "changedPropIds":["_pages_location.pathname","_pages_location.search"]}

        args = {
            "credentials": "same-origin",
            "headers": {
                "Accept": "application/json",
                "Content-Type": "application/json"
            },
            "method": "POST",
            "body": JSON.stringify(payload)
            }
            const result = originalFetch('../_dash-update-component', args);

            result.then((response) => {
                if (response.ok) {
                    updateCache(url.toLowerCase(), response.clone())
                }
        })
    }
}

async function preloadPages (preload) {
    for (y=0; y<preload.length; y++) {
        const url = preload[y]
        setTimeout(function () {fetchHandler(url)}, y*5000)
    }
}

In the code, you will notice that I give the ability to exclude layouts that you do not want to cache, as well as the ability to preload page layouts to the cache. Along with the demonstrated button to performs a clientside callback to “destroy” the cache (this is very important as it can cause some issues especially with diagnosing issues with layouts)


Hope you guys can enjoy the speed boost and less server traffic from utilizing this caching mechanism.

Please note:

  • Caching can cause hard to diagnose issues because the info is stored in the browser and will load from there, make sure you have some mechanism to clear the cache if you need to. Often a hard reload will clear caches.

  • If you have private data that would be stored in a layout, be sure that you are clearing the data, you can either do this by deleting the specific key or removing the whole cache more info here.

  • If your data is dynamically loading, there are still ways to use caching, you can split up the data to load via a callback when the layout is initially loaded.

  • To utilize caching onto the browser, the site needs to either be https or localhost, otherwise the cache will just store to a window object, being destroyed upon refresh.

7 Likes