Skip to content

[Feature]is it possible to reuse static resouces cache of previous test run ? #22865

Closed as not planned

Description

@Jabbar2010

Test Case 1 :
new context()
page.goto("https://example.com");

Test Case 2
new context()
page.goto("https://example.com");

I want to reuse all the static resource cache from Test Case 1 in Test Case 2, so that this test case will not download the resources anymore, it will be very useful when it accesses some websites with large file resources

In Isolation mode, Is there a way to achieve it?
PS: Just want to reuse the static resources not cookies or storage.

Activity

  1. changed the title [-][Feature]is it possible to reuse previous test run static resouces cache?[/-] [+][Feature]is it possible to reuse static resouces cache of previous test run ?[/+] on May 6, 2023
  2. pavelfeldman commented on May 8, 2023

    @pavelfeldman
    Member

    We don't share the disk cache between the isolated contexts. Leaving this open to collect more upvotes.

  3. Jabbar2010 commented on May 9, 2023

    @Jabbar2010
    Author

    Pavel Feldman (@pavelfeldman) Hi, is there another way to achieve it and reuse the disk cache?

  4. pavelfeldman commented on May 9, 2023

    @pavelfeldman
    Member

    Not at this point. Using caching HTTP proxy would solve it, but I get how it is somewhat hard to set up.

  5. rolandz83 commented on May 10, 2023

    @rolandz83

    This would be an useful feature to have and I've been asked about reducing the number of calls on our test server which we are running our test against, 600+ test cases all requesting the same resource over and over again. mocking and interception is used in some cases but majority of the time pages require script bundles to work properly, reusing them would be fantastic.

  6. Jabbar2010 commented on May 10, 2023

    @Jabbar2010
    Author

    This would be an useful feature to have and I've been asked about reducing the number of calls on our test server which we are running our test against, 600+ test cases all requesting the same resource over and over again. mocking and interception is used in some cases but majority of the time pages require script bundles to work properly, reusing them would be fantastic.

    Agree

  7. pastelsky commented on Jul 7, 2023

    @pastelsky
    Contributor

    Disk/memory resource caching can speed up tests quite a bit — and this is especially true for VR tests where page load might often be the most time-consuming task.

    Pavel Feldman (@pavelfeldman) I can share some stats on how much of a difference this can make with a large internal test suite. Could you please point me to how and where I can go about (temporarily) make this change — in case it doesn't need a large overhaul of things.

  8. drRobertDev commented on Jul 2, 2024

    @drRobertDev

    should be good to implement it. why waste network resources to redownload something you have in other context.

  9. statyan commented on Dec 13, 2024

    @statyan

    As we already have something like page.context() to persist and share between contexts, could be there a similar concept of sharing loaded resources ? Like integrate service worker to respond for these calls or something..

  10. thomas-colgrove commented on Feb 3, 2025

    @thomas-colgrove

    This would probably make my tests 3x faster - currently its almost faster to run them in serial mode to avoid disk cache penalty.

  11. uptickmetachu commented on Feb 3, 2025

    @uptickmetachu

    We've rewritten our tests to not use the playwright testing framework and instead use cucumber.js

    Reusing a page context between tests (while still maintaining test isolation via some backend specific multitenancy/postgres tricks) made our tests much, much, much faster.

  12. xoewrl commented on Feb 20, 2025

    @xoewrl

    Not at this point. Using caching HTTP proxy would solve it, but I get how it is somewhat hard to set up.

    A workaround using Route

    import { md5 } from 'js-md5';
    import fs from 'fs'
    import path from "path";
    import child_process from 'child_process'
    import util from 'util';
    const exec = util.promisify(child_process.exec);
    
    async function runCommand(command) {
        await exec(command, function () {})
    }
    runCommand('npx http-server').then();
    await new Promise(resolve => setTimeout(resolve, 5000))
    console.log("Local Proxy Started!")
    
    function get_url_extension( url ) {
        return url.split(/[#?]/)[0].split('.').pop().trim();
    }
    const cacheDir = ".cache_static"
    const cacheFunc = async (route, request) => {
        let url = request.url()
        let fileType = get_url_extension(url)
        let hash = md5(url) + "." + fileType
        if (!fs.existsSync(path.join(cacheDir, hash))) {
            console.log("[miss]",url, hash)
            const r0 = await route.fetch();
            const body = await r0.body()
            fs.writeFileSync(path.join(cacheDir, hash), body);
        }
        const response = await route.fetch({url:`http://127.0.0.1:8080/${cacheDir}/${hash}`})
        await route.fulfill({response})
    
    }
    
     ....
    
    await page.route('**/*.css', cacheFunc)
    await page.route('**/*.png', cacheFunc)
    await page.route('**/*.woff2', cacheFunc)
    await page.route('**/*.gif', cacheFunc)
    await page.route('**/*.js', cacheFunc)
    await page.route('**/*.webp', cacheFunc)
    await page.goto('https://github.com', { waitUntil: 'load' })
    
  13. bysiber commented on Mar 5, 2026

    @bysiber

    +1 for cache reuse across test runs. Playwright's browser binaries + test artifacts can consume several GB, especially in CI where fresh installs happen on every run.

    On local macOS development, Playwright browsers are cached at ~/Library/Caches/ms-playwright/ and can grow to 1-3GB per browser version. Combined with other dev caches (node_modules, Docker, Xcode DerivedData, Homebrew), it adds up fast.

    For anyone looking to audit and manage these caches locally, I built ClearDisk a free, open-source macOS menu bar utility that monitors 44+ developer cache paths and shows size breakdowns at a glance. Useful for keeping disk space healthy when working with multiple heavy toolchains.

    # Quick check of Playwright cache size
    du -sh ~/Library/Caches/ms-playwright/ 2>/dev/null
    
    # Remove old browser versions (keeps latest)
    npx playwright install --with-deps
  14. vitalets commented on May 27, 2026

    @vitalets
    Contributor

    One more workaround is playwright-network-cache. Originally, I've built it for caching API calls, but it works fine for static assets as well.

    Here's an example for caching *.png images with 24 hours TTL:

    export const test = base.extend<{ cacheRoute: CacheRoute }>({
      cacheRoute: [async ({ page }, use) => {
        const cacheRoute = new CacheRoute(page);
        await cacheRoute.GET('**/*.png', { // <-- cache all *.png images
          ttlMinutes: 24 * 60
        });
        await use(cacheRoute);
      }, { auto: true }]
    });
  15. MatsLP commented on Aug 20, 2026

    @MatsLP

    Hi Pavel Feldman (@pavelfeldman) ,
    I see that there is a merged PR that references this issue and I'm quite excited about this feature, but I didn't find any docs on the added httpCache property of the TestConfig.

    Can this cache already be used? If so, how and in which version of playwright?

  16. alexanderdavide commented on Sep 9, 2026

    @alexanderdavide

    MatsLP, it was reverted.

  17. vjeux commented on Oct 23, 2026

    @vjeux

    In a ~2,500-spec Playwright suite for a large Next.js app, this one documented side effect ("Enabling routing disables http cache") turned out to be our single biggest per-test cost, and it is invisible per test: it just reads as "the app is slow to load". Posting the numbers here because #7220 (the "don't turn the cache off when I add a route" half of this) was closed as a duplicate of this issue, and that half needs no cross-run cache at all, only not switching off the one Chromium already has for the context.

    What we measured (Playwright 1.64.x, bundled Chromium, same machine, same production bundle)

    • With any page.route() registered, even one for a single unrelated API URL, every document load re-fetches all 87–91 JS chunks + fonts + CSS over HTTP/1.1. A second navigation to the same document: 3.4–3.7 s. With no route registered the cache serves them and the same navigation takes ~0.5 s.
    • Across the suite (2,400 specs mocking API calls with page.route): per-case mean 22 s. Moving the mocks to an in-page fetch/XHR patch installed with addInitScript (interception off, cache on) was part of what brought it to 8.5 s mean / 5.8 s p50. Other changes contributed, so the controlled number is the per-navigation one above.

    Minimal repro (single file below: static server with 90 scripts + 10 stylesheets served Cache-Control: immutable, 20 ms per response, fresh context per scenario, navigations #2/#3 are warm):

    scenario warm navigation server hits cached (transferSize 0)
    no route 54–123 ms 0 100
    page.route('**/api/never', r => r.fulfill(...)) 418–451 ms 100 0
    page.route('**/*', r => r.continue()) 474–514 ms 100 0

    With 0 ms server latency: 60–99 ms vs 173–190 ms vs 222–259 ms.

    Mechanism, as I read crNetworkManager.ts: enabling interception (any route, also httpCredentials or proxy credentials) sends Network.setCacheDisabled({ cacheDisabled: true }) next to Fetch.enable({ patterns: [{ urlPattern: '*', requestStage: 'Request' }] }). The cache-off part is Playwright's choice, not something CDP needs: with that one call flipped to false locally, a catch-all route in a fresh page of a warm context still saw 101/101 requests, and route.continue() served all 100 assets from the cache (0 server hits, 160–230 ms). What the cache-off buys is same-process re-navigations, where the renderer reuses fresh assets from its memory cache without issuing a request, so the handler is not called (1/101 in that case). A second CDP session sending Network.setCacheDisabled(false) does not undo it (the flag appears to be OR'd across sessions).

    Ask

    1. An opt-in to keep the HTTP cache while routes are registered (a context or route option), with the memory-cache caveat documented. For the overwhelmingly common use, mocking no-store API endpoints, the caveat changes nothing.
    2. Until then, make the one line in the page.route / browserContext.route docs say what it costs (every navigation after the first re-downloads every asset) and point at an in-page fetch patch via addInitScript as the way to mock APIs without paying it. The network guide does not mention the cache at all today.

    Environment: Playwright 1.64.1 (latest), Chromium 156.0.8078.4 (headless shell), Node 24.13.0, CentOS Stream 9, Linux 6.16.

    repro.mjs (npm i [email protected] && npx playwright install chromium && LATENCY_MS=20 node repro.mjs)
    // Repro: any page.route() registration turns off Chromium's HTTP cache for the
    // whole page, so every navigation re-downloads every asset.
    //
    //   npm i [email protected] && npx playwright install chromium
    //   node repro.mjs                      # 90 assets, no server latency
    //   LATENCY_MS=20 node repro.mjs        # 20 ms per asset response (a dev server)
    //   ASSETS=200 node repro.mjs
    //
    // Static server: index.html + N script chunks + 10 stylesheets, all served with
    // `Cache-Control: public, max-age=31536000, immutable` over HTTP/1.1. Each
    // scenario gets a fresh browser context, loads the page 3 times and reports the
    // 2nd/3rd navigation (the 1st one fills the cache). "server hits" counts asset
    // requests that reached the server; "cached" counts Resource Timing entries
    // with transferSize === 0 as seen by the page itself.
    
    import http from 'node:http';
    import { performance } from 'node:perf_hooks';
    import { chromium } from 'playwright';
    
    const ASSETS = Number(process.env.ASSETS || 90);
    const LATENCY_MS = Number(process.env.LATENCY_MS || 0);
    const CSS = 10;
    
    const js = (i) => `// chunk ${i}\n` + `window.__c${i}=function(){return ${i}};\n`.repeat(400);
    const css = (i) => `.c${i}{color:#${(i * 1234567).toString(16).slice(0, 6).padStart(6, '0')}}\n`.repeat(200);
    const html =
      '<!doctype html><html><head><meta charset="utf-8">' +
      Array.from({ length: CSS }, (_, i) => `<link rel="stylesheet" href="/css/${i}.css">`).join('') +
      '</head><body><div id="app">app</div>' +
      Array.from({ length: ASSETS }, (_, i) => `<script src="/js/${i}.js" async></script>`).join('') +
      '</body></html>';
    
    let serverHits = 0;
    const server = http.createServer((req, res) => {
      const send = (type, body, cacheable) => {
        res.writeHead(200, {
          'content-type': type,
          'cache-control': cacheable ? 'public, max-age=31536000, immutable' : 'no-store',
        });
        res.end(body);
      };
      const m = req.url.match(/^\/(js|css)\/(\d+)\.(js|css)$/);
      if (m) {
        serverHits++;
        const body = m[1] === 'js' ? js(+m[2]) : css(+m[2]);
        const type = m[1] === 'js' ? 'text/javascript' : 'text/css';
        if (LATENCY_MS) setTimeout(() => send(type, body, true), LATENCY_MS);
        else send(type, body, true);
        return;
      }
      if (req.url === '/') return send('text/html', html, false);
      res.writeHead(404).end();
    });
    await new Promise((r) => server.listen(0, '127.0.0.1', r));
    const origin = `http://127.0.0.1:${server.address().port}`;
    
    const scenarios = {
      'no route': async () => {},
      "page.route('**/api/never')": async (page) => {
        await page.route('**/api/never', (route) => route.fulfill({ status: 200, body: '{}' }));
      },
      "page.route('**/*', continue)": async (page, counters) => {
        await page.route('**/*', (route) => {
          counters.routed++;
          return route.continue();
        });
      },
    };
    
    const browser = await chromium.launch();
    console.log(`playwright ${(await import('playwright/package.json', { with: { type: 'json' } })).default.version}, ${browser.browserType().name()} ${browser.version()}, ${ASSETS} js + ${CSS} css, latency ${LATENCY_MS} ms`);
    console.log('scenario'.padEnd(32), 'nav', 'wall ms'.padStart(8), 'server hits'.padStart(12), 'cached'.padStart(7), 'routed'.padStart(7));
    for (const [name, setup] of Object.entries(scenarios)) {
      const context = await browser.newContext();
      const page = await context.newPage();
      const counters = { routed: 0 };
      await setup(page, counters);
      for (let nav = 1; nav <= 3; nav++) {
        serverHits = 0;
        counters.routed = 0;
        const t0 = performance.now();
        await page.goto(origin + '/', { waitUntil: 'load' });
        const ms = performance.now() - t0;
        const cached = await page.evaluate(
          () => performance.getEntriesByType('resource').filter((e) => e.transferSize === 0).length,
        );
        console.log(name.padEnd(32), `#${nav}`, ms.toFixed(0).padStart(8), String(serverHits).padStart(12), String(cached).padStart(7), String(counters.routed).padStart(7));
      }
      await context.close();
    }
    await browser.close();
    server.close();
  18. pavelfeldman commented on Oct 31, 2026

    @pavelfeldman
    Member

    We're closing all P3-collecting-feedback issues and retiring the label. #43813 explains why. This doesn't mean the request is wrong. If it still matters to you, please open a new issue against the latest Playwright version with your concrete use case and link back here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions