GithubHelp home page GithubHelp logo

next-boost / next-boost Goto Github PK

View Code? Open in Web Editor NEW
875.0 11.0 40.0 1.18 MB

Add a cache layer for server-side-rendered pages with stale-while-revalidate. Can be considered as an implementation of next.js's Incremental Static Regeneration which works with getServerSideProps.

JavaScript 6.57% TypeScript 93.43%
nextjs ssr cache typescript swr

next-boost's Introduction

CI Coverage Status Maintainability

next-boost

next-boost adds a cache layer to your SSR (Server-Side Rendering) applications. It was built originally for Next.js and should work with any node.js http.Server based application.

next-boost achieves great performance by rendering webpages on worker_threads while serving the cached on the main thread.

If you are familiar with Next.js, next-boost can be considered as an implementation of Incremental Static Regeneration which works with getServerSideProps. And it's not meant to be used with getStaticProps, in which Next.js will do the cache for you.

$ yarn add @next-boost/next-boost
$ yarn add @next-boost/redis-cache # using load-balancer and cluster
$ yarn add @next-boost/hybrid-disk-cache # simple site with disk cache

Features

  • Drop-in replacement for Next.js's production mode: next start
  • Greatly reducing the server TTFB (time-to-first-byte)
  • Non-blocking main process for cache-serving and using worker_threads for SSR
  • Simultaneous requests with the first one rendered and the rest served from the cache
  • Small footprint with less than 200 LOC
  • Used in production with 300K pages cached
  • 2 official cache adapters

Use next-boost cli with Next.js

After install the package, just change the start script from next start to next-boost. All next start's command line arguments, like -p for specifing the port, are compatible.

 "scripts": {
    ...
    "start": "next-boost", // previously `next start`
    ...
  },

Examples

There's an example under examples/nodejs, which works with a plain http.Server.

To use it with express.js and next.js, please check examples/with-express.

Performance

By using worker_threads, the CPU-heavy SSR rendering will not blocking the main process from serving the cache.

Here are the comparision of using ApacheBench on a blog post fetched from database. HTML prerendered and the db operation takes around 10~20ms. The page takes around 200ms for Next.js to render.

$ /usr/local/bin/ab -n 200 -c 8 http://127.0.0.1:3000/blog/posts/2020/3/postname

Not a scientific benchmark, but the improvements are visibly huge.

with next start (data fetched with getServerSideProps):

Document Length:        76424 bytes
Concurrency Level:      8
Time taken for tests:   41.855 seconds
Complete requests:      200
Failed requests:        0
Total transferred:      15325600 bytes
HTML transferred:       15284800 bytes
Requests per second:    4.78 [#/sec] (mean)
Time per request:       1674.185 [ms] (mean)
Time per request:       209.273 [ms] (mean, across all concurrent requests)
Transfer rate:          357.58 [Kbytes/sec] received

with the drop-in next-boost cli:

Document Length:        78557 bytes
Concurrency Level:      8
Time taken for tests:   0.149 seconds
Complete requests:      200
Failed requests:        0
Total transferred:      15747600 bytes
HTML transferred:       15711400 bytes
Requests per second:    1340.48 [#/sec] (mean)
Time per request:       5.968 [ms] (mean)
Time per request:       0.746 [ms] (mean, across all concurrent requests)
Transfer rate:          103073.16 [Kbytes/sec] received

It even outperforms next.js's static generated page (getStaticProps), handling 2~2.5x requests per seconds in my environment.

How it works

next-boost implements a server-side cache in the manner of stale-while-revalidate. When an expired (stale) page is accessed, the cache will be served and at the same time, a background process will fetch the latest version (revalidate) of that page and save it to the cache.

The following config will cache URIs matching ^/blog.*. Only pages match rules will be handled by next-boost and there's no exclude rules.

module.exports = {
  rules: [{ regex: '^/blog.*', ttl: 300 }],
}

There are 2 parameters to control the behavior of the cache:

  • ttl (time-to-live): After ttl seconds, the cache will be revalidated. And a cached page's ttl will be updated when a page is revalidated.
  • tbd (time-before-deletion): When a page is not hit again in ttl + tbd seconds, it will be completely remove from cache.

Above: only caching pages with URL start with /blog.

Advanced Usages

Deleting/Revalidating a Single URL

By sending a GET with header x-next-boost:update to the URL, the cache will be revalidated. And if the page doesn't exists anymore, the cache will be deleted.

$ curl -H x-next-boost:update https://the_server_name.com/path_a

Batch Deleting/Revalidating

If you want to delete mutiple pages at once, you can run SQL on the cache directly:

sqlite3 /cache_path/cache.db "update cache set ttl=0 where key like '%/url/a%';"

This will force all urls containing /url/a to be revalidated when next time accessed.

Deleting cache_path will remove all the caches.

Filtering Query Parameters

By default, each page with different URLs will be cached separately. But in some cases you would like, /path_a?utm_source=twitter to be served with the same contents of /path_a. paramFilter is for filtering the query parameters.

// in .next-boost.js
{
  ...
  paramFilter: (p) => p !== 'utm_source'
}

Changing the cache key

By default, the URL will be used as the key for cached pages. If you want to server pages from different domains or by different user-agent, you can use this function to custom the cache key.

Notes:

  • If there's any exception thrown from this function, or the returned value is not a string, your server will crash.
// in .next-boost.js
{
  ...
  cacheKey: (req) => (req.headers.host || '') + ':' + req.url
}

Using rules resolver

Alternatively you can provide a function instead of array inside your config.

// in .next-boost.js
{
  ...
  rules: (req) => {
      if (req.url.startsWith('/blog')) {
          return 300
      }
  }
}

Function should return valid ttl for the request. If the function returns 0 or falsy value the request will not be cached.

The power that comes from this method is that you can decide if the request is cached or not more dynamically.

For example you can automatically ignore all request from authenticated users based on the header:

// in .next-boost.js
{
  ...
  rules: (req) => {
      if (req.headers.authorization) {
          return false
      }

      return 10 // cache all other requests for 10 seconds
  }
}

You can also get more complex rules done more easily then through regex. For example you wish different ttl for each of the pagination pages.

// in .next-boost.js
{
  ...
  rules: (req) => {
    const [, p1] = url.split('?', 2)
    const params = new URLSearchParams(p1)

    return {
        1: 5000,
        2: 4000,
        3: 3000,
        4: 2000
    }[params.get('page')] || 1000
  }
}

While you would need to write complex regex rule or potentially more rules it is easy to do it through JS logic.

In the end if you prefer writting regex but wish to leverage JS logic you can always regex match inside a rules handler.

All Configurable Options

If available, .next-boost.js at project root will be used. If you use next-boost programmatically, the filename can be changed in options you passed to CachedHandler.

tips: If you are using next-boost cli with Next.js, you may want to use the config file.

And here's an example .next-boost.sample.js in the repo.

interface HandlerConfig {
  filename?: string
  quiet?: boolean
  cache?: {
    ttl?: number
    tbd?: number
    path?: string
  }
  rules?: Array<URLCacheRule> | URLCacheRuleResolver
  paramFilter?: ParamFilter
  cacheKey?: CacheKeyBuilder
}

interface URLCacheRule {
  regex: string
  ttl: number
}

type URLCacheRuleResolver = (req: IncomingMessage) => number

type ParamFilter = (param: string) => boolean
type CacheKeyBuilder = (req: IncomingMessage) => string

Logging

Logging is enabled by default. If you use next-boost programmatically, you can disable logs by passing the quiet boolean flag as an option to CachedHandler.

...
const cached = await CachedHandler(args, { quiet: true });
...

There's also a --quiet flag if you are using the command line.

Limitations

  • For architecture with multiple load-balanced rendering servers, the benefit of using next-boost is limited. Until the url is hit on every backend server, it can still miss the cache. Use reverse proxy with cache support (nginx, varnish etc) for that.
  • GET and HEAD requests only.
  • worker_threads is used and it is a node.js 12+ feature.

FAQs

Notice about Next.js's custom server

next-boost works as an in-place replacement for next start by using Next.js's custom server feature.

On the linked page above, you can see the following notice:

Before deciding to use a custom server please keep in mind that it should only be used when the integrated router of Next.js can't meet your app requirements. A custom server will remove important performance optimizations, like serverless functions and Automatic Static Optimization.

next-boost is meant to be used on cloud VPS or containers, so serverless function is not an issue here. As to Automatic Static Optimization, because we are not doing any app.render here, it still works, as perfect as always.

Why SQLite

Here's the article about when not to use SQLite. And for next-boost's main purpuse: super faster SSR on low-cost VPSs, as far as I know, it is the best choice.

License

MIT. Copyright 2020 Rakuraku Jyo.

next-boost's People

Contributors

dependabot[bot] avatar itjesse avatar mrchief avatar olikami avatar pcampeau avatar rjyo 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  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

next-boost's Issues

revalidating the entire cache

Hey, first, thanks for this library. it's super helpful!

Second, a question;
I have a webhook on my CMS that trigger the cache validation via api for single urls as described here: https://github.com/rjyo/next-boost#deletingrevalidating-a-single-url โœ…
But I also have shared content in the CMS, say, "global". so I need to revalidate (or invalidate) the cache for all pages when that specific content is updated.
For doing so, I create a next.js api endpoint called /api/clear-cache that is called from the webhook on my CMS.
Inside that clear-cache.js I have the following code:

import { exec } from 'child_process';

const clearCache = (req, res) => {
  // this is similar to https://github.com/rjyo/next-boost#batch-deletingrevalidating but just applying it to all
  exec('sqlite3 /cache_path/cache.db "update cache set ttl=0"');
  res.writeHead(307, { Location: '/' });
  res.end();
};

export default clearCache;

this partially works, but I have to access the page twice: first time I see the cached content, second time I finally see the newest content.

Two questions:

  1. Does it mean that the TTL for now on will be 0 after executing exec('sqlite3 /cache_path/cache.db "update cache set ttl=0"');, unless I restart my node server? (My current TTL config is 7 days)
  2. How can I force the pages to be definitely revalidated/regenerated on the first time they are accessed after clearing the cache?

Thank you again!

Cache path based in domain

Hello @rjyo

We can use the domain based cache path, making it more adaptive for whitelabel solutions (example), what do you think?

TypeError: Cannot assign to read only property 'children' of object '#<Object>'

Hello! I wanted to let you know about an issue we ran into in one of our projects that was using next-boost.

Lots of details can be found here in this issue: vercel/next.js#24783 (comment)

I've created an example reproduction case here: atdrago/repro-24783@1ea71f6

The error is below, and goes away when setting NODE_ENV to production in our .env.production file, so it appears that, by default, next-boost runs with a NODE_ENV of development. This is different from next start.

The actual error we see in production is:

TypeError: Cannot assign to read only property 'children' of object '#'
at /path/to/project/.next/server/chunks/6859.js:740:29
at /path/to/project/node_modules/react/cjs/react.development.js:1067:17
at mapIntoArray (/path/to/project/node_modules/react/cjs/react.development.js:964:23)
at mapIntoArray (/path/to/project/node_modules/react/cjs/react.development.js:1004:23)
at Object.mapChildren [as map] (/path/to/project/node_modules/react/cjs/react.development.js:1066:3)
at Head.makeStylesheetInert (/path/to/project/.next/server/chunks/6859.js:732:36)
at Head.render (/path/to/project/.next/server/chunks/6859.js:789:23)
at processChild (/path/to/project/node_modules/react-dom/cjs/react-dom-server.node.development.js:3450:18)
at resolve (/path/to/project/node_modules/react-dom/cjs/react-dom-server.node.development.js:3270:5)
at ReactDOMServerRenderer.render (/path/to/project/node_modules/react-dom/cjs/react-dom-server.node.development.js:3753:22)

Question: content-length

Hi,

I'm sorry if it is obvious, but since I'm newbie at server part, I'm trying to understand what was your idea about removing content-length header. Why is it necessary to remove it?

Future plan

Features

  • v0.7.x
    • Able to filter the query parameters: /a?utm_source=google -> /a
    • Parameters control page behavior, such as ?page=2 should be kept
    • Add a callback function with input of req and output of the key to use

Intro articles

  • Posts
    • An introduction of this to next.js community
    • A Qiita post in Japanese
    • A v2ex post in Chinese, maybe
  • Examples
    • And an example of using next-boost, next and express.js as the custom server here.
    • Add an example that can be one-click deployed to Zeit Now (custom server not working on Now)

User-Agent based caching

Hello there. First of all, Thank you for making our lives easier. Having a great experience with next-boost.

I have a question about using next-boost with a specific use case.
I am serving different content based on different user agents.

This is how I am achieving this

import dynamic from 'next/dynamic'
import { withUserAgent } from 'next-useragent'

const DesktopContent = dynamic(() => import('./web'))
const MobileContent = dynamic(() => import('./mobile'))
 
 
 const Home = (props) => {

  const { ua, useragent } = props
  console.log(ua, useragent);
  return (
     <div>
        { ua && ua.isMobile ? (
        <MobileContent />
        ) : (
        <DesktopContent />
        ) }
     </div>
   )
 }

export default withUserAgent(Home);

When I access first time from PC, I see DesktopContent. But when I access it from mobile, I see a flash of DesktopContent and then MobileContent is displayed.

My question is, can I leverage it to cache based on user-agents. So that I may serve cache for a specific user agent.

Any help will be highly appreciated

Wrong Readme?

Hi, I think the comment in the image below at the README.md is wrong. the return of function should be the ttl second, not millisecond. I already tested and checked the source code. The value should be in a second
image

next-boost not properly excluding certain routes

Hey, I'm not sure how best to ask but, I was wondering how to properly exclude next-boost from caching certain routes.

I've tried using a negative lookahead in the regex to exclude /api routes as shown
module.exports = { rules: [ { regex: "^(?!.*\/api\/).*$", ttl: 10 * 60, } ] };

Yet, it still displays hits and stale/updates for /api routes.

I'm sure Im missing something simple.

Thank you!

Cache should be configurable

Hi Rjyo,
Thank to great work you have done. I think we should make the cache configurable so we can use other cache storage like redis, files.. (redis in my case) instead of default hybrid-disk-cache
I would like to hear your thoughts on this.

Error saving payload to cache Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

i'm getting following error:

Error saving payload to cache Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

there seems to be an issue in this try / catch block:

2021-12-22 at 17 52 09@2x

respectively in the serve function here:

function serve(res: ServerResponse, rv: RenderResult) {

which sets headers after the response was sent.

Custom cacheKey to cache on first dynamic url parameter should not cache second url prop to next router.

Here is an example of a config that will only cache on the first dynamic parameter:

/t/[tid]/c/[cid].js => /t/1/c/2

This will result in a cache miss on the first request with t/1/...... (tid=1) - But a hit on the second request no matter what cid parameter is.

This is done do use the first parameter as an id for dynamically created templates which should be be SSR Cached, but that template can be shared by multiple users, each with their own cid.

This is working, the only problem is that cid is also cached, meaning after the first request, the cid does not change.

What is the best way to access this cid in each request?

module.exports = {
  cacheKey: (req) => {
    if (req.url.split("/").splice(1, 1)[0] === "t") {
      return req.url.split("/").reduce((acc, s, index) => {
        return index <= 2 ? acc += s : acc
      }, "")
    } else {
      return req.url
    }
  },
  rules: [
    {
      regex: "^/t/*",
      ttl: 60, // 1 Minute
      tbd: 60 * 60 * 24 * 2 // 2 Days
    },
  ],
};

Set cache-control header

The current cache-control looks like this on a cached page:

cache-control: private, no-cache, no-store, max-age=0, must-revalidate

It probably shouldn't be private, the max-age should match the ttl, etc... Though this header doesn't seem to be set by next-boost, but it probably should be.

I think it's fine to keep a private, no-cache for a ttl=0 or if no rule is defined for the route.

When house-keeping, the db is locked

SQLite3 is locked when the cache-manager tries to remove outdated cache and release the disk space.

Took 3 mins for a database of 2GB with 300K cached pages.

This might not be a problem for smaller use cases. But for larger applications, a better solution is essential.

breaks classNames in Material ui

I tried to run it as described, but I keep running in the problem where the style sometimes disappears.

I have narrowed the problem as much as that the jss names don't match (not sure why, basically the numbers don't match up).

The problem so far only appears when loading a page directly, when using the client side navigation the styles seem to work as intended.

This is my _app.js:

import '../styles/globals.css';

import {CssBaseline, StylesProvider} from '@material-ui/core';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import {DefaultSeo} from 'next-seo';
import React from 'react';

import {generateClassName} from '../helpers/generateClassName';
import SEO from '../next-seo.config';
import settings from '../settings';

const theme = createMuiTheme({...});

function MyApp ({ Component, pageProps }) {

	React.useEffect(() => {
		// Remove the server-side injected CSS.
		const jssStyles = document.querySelector('#jss-server-side');
		if (jssStyles) {
			jssStyles.parentElement.removeChild(jssStyles);
		}
	}, []);

	return(
		<StylesProvider generateClassName={generateClassName}>
			<DefaultSeo {...SEO} />
			<ThemeProvider theme={theme}>
				<CssBaseline />
				<Component {...pageProps} />
			</ThemeProvider>
		</StylesProvider>
	);
}

export default MyApp;

And this my _documents.js:

import { ServerStyleSheets } from '@material-ui/core/styles';
import Document, { Head, Html, Main, NextScript } from 'next/document';
import React from 'react';

import {generateClassName} from '../helpers/generateClassName';

export default class MyDocument extends Document {
	render() {
		return (
			<Html lang="en">
				<Head>
					<link
						rel="stylesheet"
						href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
					/>
				</Head>
				<body>
					<Main />
					<NextScript />
				</body>
			</Html>
		);
	}
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
	// Resolution order
	//
	// On the server:
	// 1. app.getInitialProps
	// 2. page.getInitialProps
	// 3. document.getInitialProps
	// 4. app.render
	// 5. page.render
	// 6. document.render
	//
	// On the server with error:
	// 1. document.getInitialProps
	// 2. app.render
	// 3. page.render
	// 4. document.render
	//
	// On the client
	// 1. app.getInitialProps
	// 2. page.getInitialProps
	// 3. app.render
	// 4. page.render

	// Render app and page and get the context of the page with collected side effects.

	const sheets = new ServerStyleSheets({
		serverGenerateClassName: generateClassName
	});
	const originalRenderPage = ctx.renderPage;

	ctx.renderPage = () =>
		originalRenderPage({
			enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
		});

	const initialProps = await Document.getInitialProps(ctx);

	return {
		...initialProps,
		// Styles fragment is rendered after the app and page rendering finish.
		styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
	};
};

The problem appears when I add custom styles through className and makeStyles.

Redis/Async cache adapter

Hello,

I was looking into implementation of custom cache adapter. We would like to try and use redis for it but I have few questions.

As I see from example adapter all the adapter methods get, has etc.. need to be synchronous?

I believe that with redis (and most of the other storage mechanisms) fetching something from storage is not synchronous but asynchronous.

serveCache method is already async and returns Promise so I was thinking that if we would assume that all cache adapter methods (get, has..) are async we would not break current implementation because sync adapters would also resolve await get() (if get function is sync await will resolve instantly).

Any thoughts on this?

Tnx!

Authentication and cashing - ignore some paths

Is there any solution to use this with auth0 in SSR?

src/pages/api/auth/[...auth0].ts
import { handleAuth } from '@auth0/nextjs-auth0';

export default handleAuth();

I have /api/auth pages to authenticate the user and looks like that session is shared/cached

next.js@12 support?

hey there ~

next-boost@latest currently is not compatible with [email protected]:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/next
npm ERR!   next@"^12.0.1" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer next@"^11.0.0" from [email protected]
npm ERR! node_modules/next-boost
npm ERR!   next-boost@"*" from the root project

is there any plan for that?

504 code status some time

Hello, i have a problem with my testing server.
when i deploy and not used sometime, like as 3-5 hours, my host has status 504
my config for next-boost

module.exports = {
    rules: [
        {
            regex: '.*',
            ttl: 180,
            tbd: 300
        },
    ],
}

help only rebuild project

In logs i see. error timeout
ะกะฝะธะผะพะบ ัะบั€ะฐะฝะฐ 2021-07-15 ะฒ 14 04 31 (2)

The tdb value in .next-boost.js config is ignored for the default adapter.

Description

The the tdb value is ignored in .next-boost.js

We are using the following configuration for our .next-boost.js file

module.exports = {
  cache: {
    ttl: 60,
    tbd: 60 * 2,
  },

Expected behavior
Cache manager inited, will start to purge in 120s

Actual Behavior
Cache manager inited, will start to purge in 3600s

Notes

What we have observed is that the tbd value is ignored, and instead it uses the default value or 1 hour. We have been able to work around this by utilizing the ability to set a custom adapter, but still use the hybrid-disk-cache.

I haven't been able to 100% confirm it but I believe the following line of code is the problem. As I don't believe the configuration values are being passed as a parameter into the init() function.

See -

const cache = await adapter.init()

  const cache = await adapter.init()

And it should be

const cache = await adapter.init(conf)

SQLite Cache implementation doesn't work in Google Cloud Run

I've just tried to deploy a container running a next.js app with next-boost into Google Cloud Run and it seems that due to the partial implementation of system calls in the gVisor container sandbox the SQLite cache is unable to create the cache table.

Further digging with strace suggests that fcntl(20, F_GETLK, 0x3ea4ad89e6e0) = -1 EINVAL (Invalid argument) is the culprit.

Given that Cloud Run's 'filesystem' is simulated from the memory provided to the container it would be nice if there was an option to configure an 'in-memory' cache rather than the default hybrid-disk-cache.

Any suggestions or guidance on how to implement an alternative cache and provide configuration in next-boost would be much appreciated and I can attempt a PR.

Original Req and Res objects are replaced when passed to renderer

Issue:

Within getServerSideProps, we are trying to access our res.locals and req custom variables that are needed to render the page properly. However, when next-boost is populating the cache for first time via the Renderer, everything is stripped except for req.url, req.method, req.headers:

      const args = { path: req.url, headers: req.headers, method: req.method }
      const rv = await renderer.render(args)
      // rv.body is a Buffer in JSON format: { type: 'Buffer', data: [...] }
      const body = Buffer.from(rv.body)

Is there anyway to pass the original req and res objects that are sent to the CacheHandler? If not, are there any suggestions for work arounds?

Crash on localhost:3000

Hi, i have a strange problem with my next js app when i use next-boost. If i use this on localhost:3000 i get any time random fatal error and my app crash. But when i add this localhost:3200 its working good. How can i fix this problem on port 3000?
https://i.imgur.com/G6DKB03.png

Filter query parameters

Plan

As mention in #1

Add a new API for filtering out the unwanted parameters:

in the config file .next-boost.js, if we add the following config:

module.exports = {
  ...
  paramFilter: (p) => {
    p === 'fbclid' || p.startsWith('utm_') ? false : true
  },
}

In paramFilter, we just return true for the params you want to keep and false for those you want to remove.

With the filter, /path?fbclid=abc will be transformed to /path before searching the cache.

Effect

Before:

No paramFilter

  • miss: /url_a
  • miss: /url_a?utm_source=facebook

After:

with paramFilter:

(p) => !p.startsWith('utm_')
  • miss: /url_a
  • hit: /url_a?utm_source=facebook

Option to filter out some headers from the cache

We have a paramFilter config option which allows us to filter out some params that do not have impact on cache but in turn are important eg. utm_ params and such.

When working with analytics there is sometimes need to set custom cookies on initial request that then identify user across sessions. Those cookies do not impact the response of the page (or cache in that matter) in any way. Problem is that if we set cookies on server side then Set-Cookie header will be included inside the cache.

My proposition is that we add headerFilter config that would like paramFilter skip adding matched cookies to the cache.

I think it would be easy to integrate on this line before saving to cache:
https://github.com/next-boost/next-boost/blob/master/src/handler.ts#L80

I would be open to contribute to this if this makes sense?

There is try-catch missed in `waitAndServe`

/Users/SM0397/Public/www/official-website-next/node_modules/next-boost/dist/cache-manager.js:32
            payload.headers = JSON.parse((yield cache.get('header:' + key)).toString());
                                                                            ^

TypeError: Cannot read properties of undefined (reading 'toString')
    at /Users/SM0397/Public/www/official-website-next/node_modules/next-boost/dist/cache-manager.js:32:77
    at Generator.next (<anonymous>)
    at fulfilled (/Users/SM0397/Public/www/official-website-next/node_modules/next-boost/dist/cache-manager.js:5:58)

async function waitAndServe(key: string, cache: Cache): Promise<State> {

Maybe, use optional-chaining is a more quickly way.
I'm a little confuse, decodePayload in other function of this file has try-catch but hasn't in waitAndServe

image

version: 0.12.0

Getting Error while running next-boost

changed my start script

"scripts": {
    "dev": "next dev --port 8080",
    "build": "next build",
    "start": "next-boost"
  },

now running npm start gives me this

internal/modules/cjs/loader.js:638
throw err;
^

Error: Cannot find module 'worker_threads'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:636:15)
at Function.Module._load (internal/modules/cjs/loader.js:562:25)
at Module.require (internal/modules/cjs/loader.js:692:17)
at require (internal/modules/cjs/helpers.js:25:18)
at Object. (/Users/yasserzubair1/projects/graana-nextjs-ssr/node_modules/multee/index.js:2:12)
at Module._compile (internal/modules/cjs/loader.js:778:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)

Any help is appreciated

(Question) dev server and custom response on specific routes

Hi ๐Ÿ‘‹, so here's my current next server which is based on express:

const express = require("express");
const next = require("next");
const path = require("path");

const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  server.use("/something", express.static(path.join(__dirname, "public/static/something")));

  server.all("*", (req, res) => handle(req, res));

  server.listen(port, (err) => {
    if (err) throw err;

    console.log(`> Ready on http://localhost:${port}`);
  });
});

I'm using this server for both production and local development so I always have access to my custom route /something. I'm wondering how I could port this server to next-boost since it seems to only be for production?

Unclear how next-boost interacts with Next.js Incremental Static Regeneration

I am using the next-boost with express.js example in this case. Part of my website includes a completely static blog which is served from a headless CMS. Next.js recommends its Incremental Static Regeneration feature for this. So I have:

pages/blog.js

getStaticProps() {
  let articles = CMSAPI();
  return { props: { articles }, revalidate: 30};
}

pages/blog/[article.js]
getStaticPaths() { let articles = CMSAPI(); return { paths : { articles } }; } getStaticProps({article}) { return { props: {article}, revalidate: 30 }; }

What I am not sure of:

  1. Does Next.js ISR even work when using next-boost + express.js?
  2. When next-boost cache has a miss or stale for one of those pages, will it just return the most recent version of the page next.js already has (re)generated or will it invoke getStaticPaths again?
  3. If I was to switch everything to getServerSideProps, is there a way in next-boost to pregenerate all pages once? I want to avoid a user landing on a blog/some-article page after it has been purged from cache and incur the high latency of the first Server Side Generation.

Memory Leak Hybrid disk cache

image
Hi, sometime, maybe 1 in 3 days i has a memory leak
in container log haven't any issues.

Next 12.0.7
Next-boost latest
Production

const Adapter = require('@next-boost/hybrid-disk-cache').Adapter

module.exports = {
    rules: [
        {
            regex: '.*',
            ttl: 300,
        },
    ],
    cacheAdapter: new Adapter({
        path: '/tmp/hdc',
        ttl: 180,
        tbd: 3600,
    }),
    metrics: true,
}

SqliteError: disk I/O error on DigitalOcean App platform

I'm trying to use next-boost with an app deployed to DO App Platform (in a Docker container), but I get the following error:

[2021-08-23 08:57:50] > next-boost -H 0.0.0.0 -p ${PORT:-8080}
[2021-08-23 08:57:50] 
[2021-08-23 08:57:50] > Preparing cached handler
[2021-08-23 08:57:50]   Loaded next-boost config from .next-boost.js
[2021-08-23 08:57:50] (node:17) UnhandledPromiseRejectionWarning: SqliteError: disk I/O error
[2021-08-23 08:57:50]     at new Cache (/workspace/node_modules/hybrid-disk-cache/dist/index.js:64:27)
[2021-08-23 08:57:50]     at Object.init (/workspace/node_modules/next-boost-hdc-adapter/dist/index.js:18:19)
[2021-08-23 08:57:50]     at Object.<anonymous> (/workspace/node_modules/next-boost/dist/handler.js:81:77)
[2021-08-23 08:57:50]     at Generator.next (<anonymous>)
[2021-08-23 08:57:50]     at /workspace/node_modules/next-boost/dist/handler.js:8:71
[2021-08-23 08:57:50]     at new Promise (<anonymous>)
[2021-08-23 08:57:50]     at __awaiter (/workspace/node_modules/next-boost/dist/handler.js:4:12)
[2021-08-23 08:57:50]     at Object.CachedHandler [as default] (/workspace/node_modules/next-boost/dist/handler.js:76:12)
[2021-08-23 08:57:50]     at /workspace/node_modules/next-boost/dist/next/server.js:28:43
[2021-08-23 08:57:50]     at Generator.next (<anonymous>)
[2021-08-23 08:57:50] (node:17) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
[2021-08-23 08:57:50] (node:17) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
[]

My config looks like so:

module.exports = {
  cache: {
    path: '.cache/',
    ttl: 172800,
    tbd: 3600 * 24 * 5,
  },
  rules: [{ regex: '^/.*', ttl: 172800 }], // cache everything for 2 days
}

I tried changing the path to the /tmp directory, with no success.

TypeError: Cannot read property 'toString' of undefined

node_modules\next-boost\dist\cache-manager.js:32
            payload.headers = JSON.parse((yield cache.get('header:' + key)).toString());
                                                                            ^

TypeError: Cannot read property 'toString' of undefined

I have no idea why it's happening. Any suggestions? I wouldn't expect my app to crash because the cache doesn't get a key. Is it a bug, or there's something I should do, maybe some configuration?

next-boost and vercel

Hi!

First of all thank you for this great package. We're currently considering moving a gatsby site to next and this package seems perfect to decrease page load time.

Could you advise on the best way to deploy this? As I understand vercel only offers serverless functions which would not work with the custom server approach if I'm not mistaken?

Thanks in advance and happy easter :)

Subdomain support

I just switched over my project to next.js so looking to take advantage of getServerSideProps + caching. This seemed like a great project as compared to writing your own code or cacheable-response. In my use case, each page has multiple subdomain versions where the content is populated based on a headless CMS.

In my server.js:

routes.forEach(route => {
		server.get(route.url, (req, res) => {
			let query = req.query;
			if(req.subdomains.length > 0 && req.subdomains[0] !== 'www')
				query["subdomain"] = req.subdomains[0]

			return handler(req, res, route.page, query)
		})
	})

So in my page, serverSideProps:

export async function getServerSideProps(context) {
    let props = getDataFromCMS(context.query.subdomain);
    return { props };
}

The URL however doesn't change so I am wondering how to enable caching each subdomain version of the page. Happy to also try a PR if you can give me some guidance on where to start.

Server requirements

For the nextjs app the docker node:lts-alpine3.12 is enough to run project. But with this boost additional packages are required.
Can we have a list of all specs needed?
What it missing probably is the python

error when building docker

error when building docker when next boost is installed

error /app/node_modules/better-sqlite3: Command failed.
Exit code: 1
Command: prebuild-install || npm run build-release
Arguments:
Directory: /app/node_modules/better-sqlite3
Output:
prebuild-install WARN install No prebuilt binaries found (target=14.17.1 runtime=node arch=x64 libc=musl platform=linux)
npm WARN lifecycle The node binary used for scripts is /tmp/yarn--1624018369193-0.05271893516712556/node but npm is using /usr/local/bin/node itself. Use the --scripts-prepend-node-path option to include the path for the node binary npm was executed with.

[email protected] build-release /app/node_modules/better-sqlite3
node-gyp rebuild --release

gyp ERR! find Python
gyp ERR! find Python Python is not set from command line or npm configuration
gyp ERR! find Python Python is not set from environment variable PYTHON
gyp ERR! find Python checking if "python" can be used
gyp ERR! find Python - "python" is not in PATH or produced an error
gyp ERR! find Python checking if "python2" can be used
gyp ERR! find Python - "python2" is not in PATH or produced an error
gyp ERR! find Python checking if "python3" can be used
gyp ERR! find Python - "python3" is not in PATH or produced an error
gyp ERR! find Python
gyp ERR! find Python **********************************************************
gyp ERR! find Python You need to install the latest version of Python.
gyp ERR! find Python Node-gyp should be able to find and use Python. If not,
gyp ERR! find Python you can try one of the following options:
gyp ERR! find Python - Use the switch --python="/path/to/pythonexecutable"
gyp ERR! find Python (accepted by both node-gyp and npm)
gyp ERR! find Python - Set the environment variable PYTHON
gyp ERR! find Python - Set the npm configuration variable python:
gyp ERR! find Python npm config set python "/path/to/pythonexecutable"
gyp ERR! find Python For more information consult the documentation at:
gyp ERR! find Python https://github.com/nodejs/node-gyp#installation
gyp ERR! find Python **********************************************************
gyp ERR! find Python
gyp ERR! configure error
gyp ERR! stack Error: Could not find any Python installation to use
gyp ERR! stack at PythonFinder.fail (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:307:47)
gyp ERR! stack at PythonFinder.runChecks (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:136:21)
gyp ERR! stack at PythonFinder. (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:179:16)
gyp ERR! stack at PythonFinder.execFileCallback (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/find-python.js:271:16)
gyp ERR! stack at exithandler (child_process.js:326:5)
gyp ERR! stack at ChildProcess.errorhandler (child_process.js:338:5)
gyp ERR! stack at ChildProcess.emit (events.js:375:28)
gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12)
gyp ERR! stack at onErrorNT (internal/child_process.js:467:16)
gyp ERR! stack at processTicksAndRejections (internal/process/task_queues.js:82:21)
gyp ERR! System Linux 5.8.0-55-generic
gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--release"
gyp ERR! cwd /app/node_modules/better-sqlite3
gyp ERR! node -v v14.17.1
gyp ERR! node-gyp -v v5.1.0
gyp ERR! not ok
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] build-release: node-gyp rebuild --release
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] build-release script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! /root/.npm/_logs/2021-06-18T12_12_50_147Z-debug.log

Random 504 timeouts with SYNC_LOCK

Hello,

great package, we have cut our load times drastically after integrating next-boost.

The cache is working really well, even with thousands requests per seconds (we managed to hit 40k req/s with response time still under 500ms).

While cache is working perfectly for the most time i noticed that sometimes (on various loads) SYNC_LOCK timeout of 10s expired and then that manifests as 504 errors on our web server.

I initially thought that is happening due to revalidation logic eg:

  1. Page is under intense load
  2. Cache becomes invalid
  3. First request come and it is reported as miss
  4. Next request comes back but it is blocked by SYNC_LOCK
  5. Revalidation takes some time and then expires after 10s (timeout inside library)
  6. 504 is generated

But after running some tests with longer ttl i still got the same errors (even though all requests where hits because cache was still valid).

I am thinking that we are maybe getting some anomalies with read/write from cache?

Any thoughts on this or advices?

Thanks!

MaterialUI styles not rendering

There seems to be a problem with materialUI styles not rendering at all when using next-boost.

SSR caching is really essential for nextjs and I hope this can be fixed.
I followed the instructions for SSR and nextjs here:
https://material-ui.com/styles/advanced/#next-js

The style breaks for MaterialUI TextField and the MaterialUI Switch. However when client side logic refreshes the site the styles are back to normal.

This works fine when running nextjs only (next start) instead of next-boost.
Not seeing any errors in the terminal.

Any idea what could be missing or how to narrow the problem?
Thanks alot!

Potential difference between default and custom cache adapter

Hello @rjyo,
I just started trying out the library and it looks promising so far. I have a question or maybe a bug report.

When using the .next-boost.js config file, the path to the cache can be supplied in the cache.path property. However it seems this is not passed to the default Cache.init() when cacheAdapter is not provided in the config. This results in all options using the default, path being \tmp\hdc.

> Preparing cached handler
  Loaded next-boost config from .next-boost.js
// Logging the merged conflict shows all the options
// Logging from next-boost-hdc-adapter's init shows undefined
  Cache located at \tmp\hdc
  Cache manager inited, will start to purge in 3600s

A workaround I found is to pass the adapter itself in the .next-boost.js config file:

cacheAdapter: hdcAdapter.init({
  path: path.join(__dirname, 'cache'),
}),

Could this be fixed so we don't have to create the adapter outside?

Another thing I noticed is that in Handler's close shutdown() is called on the default Cache object. Should this be called on the above created cache instead? Otherwise the provided cache's shutdown() would never be called.

Thank you!

How are cache TTLs set for cache adapter when differing in rules?

In the HDC config example:

module.exports = {
  rules: [
    {
      regex: '^/blog.*',
      ttl: 300,
    },
    {
      regex: '.*',
      ttl: 10,
    },
  ],
  paramFilter: p => {
    p === 'fbclid' || p.startsWith('utm_') ? false : true
  },
  cacheAdapter: new Adapter({
    path: '/tmp/hdc',
    ttl: 60,
    tbd: 3600,
  }),
}

If the adapter has a TTL of 60, does it ignore the two different TTLs provided by the rules value?

For example, I am using the redis cache adapter, but have this function set for rules:

rules: (req) => {
      
        if (someBoolean) {
          return cache.ttl!;
        }
        return 0;
      },

Where we are essentially skipping cache for some instances. Will this be respected by the cache adapter?

next-config compress

Hi,

First, thank you for this package.

Would it be possible to opt out gzip compresion same as default next server does? I understand, that u wanted to save compressed files to db, but still we use gzip compresion on nginx.

Randomly got blank white page

My production website randomly got a blank white page on the front page only. I need to manually clear the cache to get it back working again. I'm not sure if it's related to this issue
image

next-boost+express.js loses some of the request values

This is based on the next-boost express.js example.

When I go to one of the pages on my site, express.js is hit and generates some values based on the incoming request (ex: req.subdomains, req.query, req.params). When I attempt to use one of these value in my getServerSideProps(context) {} function, they do not appear. I assume it is due to this: https://github.com/rjyo/next-boost/blob/27ac46f3ae498ccdb405013e435685a497e22b83/src/handler.ts#L79

The workarounds are not too bad but still wanted to post so it makes it easier to continue leveraging some of the req values that express.js generates.

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.