Git Product home page Git Product logo

next-plugin-query-cache's Introduction

next-plugin-query-cache ยท codecov semantic-release

A build-time query cache for Next.js. Works by creating an HTTP server during the build that caches responses.

Quick Glance

// 1) import a configured fetch function
import queryFetch from '../query-fetch';

export const getStaticProps = async () => {
  // 2) use the fetch inside of `getStaticProps` or `getStaticPaths`.
  // This function will check the cache first before making an outbound request
  const response = await queryFetch('https://some-service.com', {
    headers: {
      accept: 'application/json',
    },
  });

  const data = await response.json();

  return { props: { data } };
};

function MyPage({ data }) {
  return // ...
}

export default MyPage;

Who is the library for?

This lib is for Next.js users who create static (or partially static) builds. The query cache saves responses as they are requested so shared queries across pages (e.g. for header data) are de-duplicated.

This is particularly useful if your Next.js site is powered by SaaS products that provide their API over HTTP like headless CMSes and headless ecommerce platforms (and even more useful if those SaaS services charge per API request ๐Ÿ˜…).

๐Ÿ‘‹ NOTE: This lib is currently not so useful for non-HTTP type of requests (e.g. database calls) since the query cache only works on an HTTP level. See #4 for more details.

Motivation

Unlike Gatsby, Next.js does not provide any sort of shared data layer. This is nice because it's simple and unopinionated. However, in large static sites with many repeated queries, there's a missed opportunity to cache results and share them between pages.

next-plugin-query-cache aims to do just that. During the build, it creates an HTTP proxy server that all concurrent build processes can go through to request a resource.

  • If the proxy already has the value, it'll return it instead of fetching it.
  • If a requested resource already has a request inflight, the proxy will wait till that request is finished instead of requesting it again.
  • There's also an optional in-memory cache made for de-duping request on a per-page level (useful for after the build in SSR or ISR).

Installation

Install

yarn add next-plugin-query-cache next@latest

or

npm i next-plugin-query-cache next@latest

NOTE: this lib requires Next ^10.0.9

Add to next.config.js

// next.config.js
const createNextPluginQueryCache = require('next-plugin-query-cache/config');
const withNextPluginQueryCache = createNextPluginQueryCache({
  /**
   * (optional) if you have a preferred port for the proxy server,
   * you can add it here. otherwise, it'll pick an ephemeral port.
   * This should not be the same port as your dev server.
   */
  port: 4000,

  /**
   * (optional) provide a flag that will disable the proxy
   */
  disableProxy: process.env.DISABLE_PROXY === 'true',

  /**
   * (optional) provide a fetch implementation that the proxy will use
   */
  fetch: require('@vercel/fetch')(require('node-fetch')),

  /**
   * (optional) provide a function that returns a string. the response
   * result will be saved under that key in the cache.
   *
   * NOTE: ensure this matches the `calculateCacheKey` implementation
   * provided in `createQueryFetch`
   */
  calculateCacheKey: (url, options) => url,
});

module.exports = withNextPluginQueryCache(/* optionally add a next.js config */);

Create the client queryFetch function

next-plugin-query-cache returns a decorated window.fetch implementation. Whenever you call this wrapped fetch, it will check the cache. If the resource is not in the cache, it will make a real request.

To create this decorated fetch function, call createQueryFetch.

// query-fetch.js
import { createQueryFetch } from 'next-plugin-query-cache';

const { queryFetch, cache } = createQueryFetch({
  /**
   * REQUIRED: paste this as is. `process.env.NEXT_QUERY_CACHE_PORT`
   * is provided by `next-plugin-query-cache`
   */
  port: process.env.NEXT_QUERY_CACHE_PORT,

  /**
   * (optional) provide an underlying fetch implementation. defaults to
   * the global fetch.
   */
  fetch: fetch,

  /**
   * (optional) provide a function that determines whether or not
   * the request should be cached. the default implement is shown here
   */
  shouldCache: (url, options) => {
    const method = options?.method?.toUpperCase() || 'GET';
    return method === 'GET' && typeof url === 'string';
  },

  /**
   * (optional) provide a function that returns whether or not to
   * use the proxy. this function should return `true` during the
   * build but false outside of the build. `process.env.CI === 'true'`
   * works in most Next.js environments
   *
   * the default implementation is shown here.
   */
  getProxyEnabled: async () =>
    (process.env.CI === 'true' ||
      process.env.NEXT_PLUGIN_QUERY_CACHE_ACTIVE === 'true') &&
    !!process.env.NEXT_QUERY_CACHE_PORT,

  /**
   * (optional) provide a function that determines whether or not
   * the in-memory cache should be used.
   *
   * the default implementation is shown
   */
  getInMemoryCacheEnabled: async () => true,

  /**
   * (optional) provide a function that returns a string. the response
   * result will be saved under that key in the cache.
   *
   * NOTE: ensure this matches the `calculateCacheKey` implementation
   * provided in `createNextPluginQueryCache`
   */
  calculateCacheKey: (url, options) => url,
});

// the cache is an ES6 `Map` of cache keys to saved responses.
// you can optionally modify the in-memory cache using this.
cache.clear();

// export the wrapped fetch implementation
export default queryFetch;

Update your build command

Assign the environmnt variable NEXT_PLUGIN_QUERY_CACHE_ACTIVE to enable query caching.

This is dependent on the default getProxyEnabled function.

// package.json
{
  "scripts": {
    "build": "NEXT_PLUGIN_QUERY_CACHE_ACTIVE=true next build"
    // ...
  }
  // ...
}

Usage

Using the queryFetch function

After you create the queryFetch function, use it like you would use the native fetch function.

When you request using this queryFetch function, it'll check the cache first during the build. That's it!

// /pages/my-page.js
import queryFetch from '../query-fetch';

export const getStaticProps = async () => {
  // NOTE: you probably only want to use the `queryFetch` inside of
  // `getStaticProps` (vs client-side requests)
  const response = await queryFetch('https://some-service.com', {
    headers: {
      accept: 'application/json',
    },
  });

  const data = await response.json();

  return { props: { data } };
};

function MyPage({ data }) {
  return (
    <div>
      <h1>My Page</h1>
      <pre>{JSON.string(data, null, 2)}</pre>
    </div>
  );
}

export default MyPage;

Debugging and logging

next-plugin-query-cache ships with a logger and a report creator to help you debug the query cache.

๐Ÿ‘‹ NOTE: The Next.js build does not provide an end build event, so we result to logging as the cache hits happen. When the build is finished, we run script that reads the logged output and aggregates the numbers. If you have ideas on how to improve this features, feel free to open an issue!

In order to run the logger and reporter, run the following command:

NEXT_PUBLIC_QUERY_CACHE_DEBUG=true npm run build | npx npqc-create-report

This will set the environment variable NEXT_PUBLIC_QUERY_CACHE_DEBUG, run the build, and then pipe the result into the reporter.

If all goes well, you'll this message:

=== Next Plugin Query Cache ===
   1952 total cache hits.
   1816 hits in memory.
    136 hits in the proxy.
      3 build processes found.
===============================

Wrote out extended report out to ../next-plugin-query-cache-2021-03-07T21:55:23.098Z.csv

FAQ

How does the proxy work?

The proxy isn't a formal proxy (e.g. SOCKS5) but instead an HTTP service that serializes the arguments and results from window.fetch Requests and Responses.

This approach works well with the provided fetch-based API and doesn't require stateful connections that other proxy protocols require. The downside to this is that the current API is only limited to serializing requests and responses with simple text as the payload bodies.

Does this work in ISR/SSR?

After the build, only the in-memory cache will work (which still does request de-duping!). The proxy is only available during the build (and is only needed during the build due to concurrent build processes).

Whether or not the queryFetch function calls the proxy is dependent on how you configure the getProxyEnabled function in the createQueryFetch call. The default implementation looks for either the process.env.CI environment variable or process.env.NEXT_PLUGIN_QUERY_CACHE_ACTIVE variable. If either of those are 'true', it'll request through the proxy.

process.env.CI is a nice one to use since it's set by Vercel during the build but isn't set during SSR/ISR etc.

next-plugin-query-cache's People

Contributors

gjsduarte avatar renovate-bot avatar renovate[bot] avatar ricokahler avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

next-plugin-query-cache's Issues

Cache more than HTTP requests

Currently, this lib only supports caching HTTP requests (primarily for the use case of SaaS services like headless CMSes)

It's possible to do more than that but that would require orchestrating your non-HTTP requests through the shared proxy process.

This would probably require a good amount of build tool dev (webpack loaders, maybe babel) so I'm creating this issue to gauge interest. Leave a ๐Ÿ‘ or comment below with your use case.

Report cache misses in reporter

I didn't consider reporting the cache misses in the reporter but I realize that that info is probably useful!

This is a todo item for myself but if you'd like to contribute, feel free to reach out!

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Rate-Limited

These updates are currently rate-limited. Click on a checkbox below to force their creation now.

  • chore(deps): update typescript-eslint monorepo to v7 (major) (@typescript-eslint/eslint-plugin, @typescript-eslint/parser)

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/release.yml
  • actions/checkout v2
  • actions/setup-node v2
  • codecov/codecov-action v2
  • ubuntu 18.04
.github/workflows/test.yml
  • actions/checkout v2
  • actions/setup-node v2
  • codecov/codecov-action v2
  • ubuntu 18.04
npm
package.json
  • @types/express ^4.17.11
  • @types/node-fetch ^2.5.8
  • express ^4.17.1
  • node-fetch ^2.6.1
  • @babel/core 7.21.4
  • @babel/plugin-transform-runtime 7.21.4
  • @babel/preset-env 7.21.4
  • @babel/preset-typescript 7.21.4
  • @rollup/plugin-babel 5.3.1
  • @rollup/plugin-node-resolve 13.3.0
  • @types/jest 26.0.24
  • @types/node 14.18.36
  • @typescript-eslint/eslint-plugin 4.28.5
  • @typescript-eslint/parser 4.28.5
  • babel-eslint 10.1.0
  • eslint 7.32.0
  • eslint-config-react-app 6.0.0
  • eslint-plugin-flowtype 5.10.0
  • eslint-plugin-import 2.27.5
  • eslint-plugin-jsx-a11y 6.7.1
  • eslint-plugin-react 7.32.2
  • eslint-plugin-react-hooks 4.2.0
  • jest 27.2.4
  • next 12.0.7
  • prettier 2.8.4
  • rollup 2.79.1
  • semantic-release 17.4.7
  • typescript 4.9.5
  • next ^10.0.9 || ^11.0.0 || ^12.0.0

  • Check this box to trigger a request for Renovate to run again on this repository

No DefinePlugin

Hi,
Afet installing an configuring accoriding to your readme, I get this:

ready - started server on http://localhost:3000
[next-plugin-query-cache] Up on port 39733.
> Ready! Available at http://localhost:3000
Error: Could not find DefinePlugin. This is an bug in next-plugin-query-cache.
    at Object.webpack (/home/ramon/Documents/cdbcn/node_modules/next-plugin-query-cache/config.cjs.js:556:19)
    at getBaseWebpackConfig (/home/ramon/Documents/cdbcn/node_modules/next/dist/build/webpack-config.js:138:395)
    at async Promise.all (index 0)
    at async HotReloader.start (/home/ramon/Documents/cdbcn/node_modules/next/dist/server/hot-reloader.js:14:2403)
    at async DevServer.prepare (/home/ramon/Documents/cdbcn/node_modules/next/dist/server/next-dev-server.js:15:414)
    at async /home/ramon/Documents/cdbcn/node_modules/next/dist/cli/next-dev.js:22:1

Build hangs in Next 10.0.9

Upgrading to Next 10.0.9 beaks the query cache plugin because this version removes the forced process.exit(0) after the build command executes:

image

pictured left is the next 10.0.8 and pictured right is next 10.0.9

We relied on this process.exit(0) to forcefully shut down the proxy cache server but without it, it causes the build to hang because the proxy server still has an on-going process.

Debugging and logging

Though the query cache works, it's hard to tell at times.

It'd be nice to get some metric on how many requests were fire and how high the cache hit rate is for particular resources. This is challenging because next.js doesn't provide a "the build is done" event.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.