├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── examples ├── express │ ├── README.md │ ├── compose.yaml │ ├── default.vcl │ ├── esbuild.config.mjs │ ├── package.json │ ├── src │ │ ├── components │ │ │ └── MyFragment.tsx │ │ ├── entry-client.tsx │ │ ├── entry-server.tsx │ │ ├── pages │ │ │ └── App.tsx │ │ └── server.tsx │ ├── tsconfig.client.json │ ├── tsconfig.json │ └── tsconfig.server.json └── next │ ├── .env │ ├── .eslintrc.json │ ├── README.md │ ├── components │ └── MyFragment.tsx │ ├── compose.yaml │ ├── default.vcl │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ └── index.tsx │ ├── public │ ├── favicon.ico │ ├── next.svg │ └── vercel.svg │ ├── server.tsx │ ├── tsconfig.client.json │ ├── tsconfig.json │ └── tsconfig.server.json ├── lib ├── jest.config.ts ├── package.json ├── src │ ├── __tests__ │ │ ├── client │ │ │ ├── __snapshots__ │ │ │ │ └── withESI.test.tsx.snap │ │ │ └── withESI.test.tsx │ │ └── server │ │ │ ├── __snapshots__ │ │ │ ├── server.test.tsx.snap │ │ │ └── withESI.test.tsx.snap │ │ │ ├── server.test.tsx │ │ │ └── withESI.test.tsx │ ├── server.tsx │ └── withESI.tsx ├── tsconfig.build.json └── tsconfig.json ├── package.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Dockerfile 3 | compose.yaml 4 | .git 5 | .gitignore 6 | *.md 7 | dist 8 | .github 9 | .vscode 10 | .next 11 | lib/lib -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2021": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:react/recommended", 12 | "plugin:react/jsx-runtime", 13 | "plugin:@typescript-eslint/recommended", 14 | "prettier" 15 | ], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaVersion": "latest", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "sourceType": "module" 23 | }, 24 | "plugins": ["@typescript-eslint"], 25 | "rules": { 26 | "react/display-name": "off" 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | }, 33 | "ignorePatterns": ["node_modules", "dist", "lib/lib", ".next"] 34 | } 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dunglas 2 | tidelift: "npm/react-esi" 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests / Lint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [16, 18, 20] 11 | name: Run tests with Node.js ${{ matrix.node-version }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Install pnpm 18 | uses: pnpm/action-setup@v3 19 | with: 20 | version: 8 21 | 22 | - name: Setup node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: "pnpm" 27 | 28 | - run: pnpm config --global set dedupe-peer-dependents=false 29 | - run: pnpm --filter=react-esi install 30 | - run: pnpm --filter=react-esi run lint 31 | - run: pnpm --filter=react-esi run typecheck 32 | - run: pnpm --filter=react-esi run build 33 | - run: pnpm --filter=react-esi run test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | .pnpm-store 8 | 9 | # testing 10 | coverage 11 | 12 | # next.js 13 | .next/ 14 | out/ 15 | 16 | # production 17 | build 18 | dist 19 | lib/lib 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | link-workspace-packages=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/.svn 3 | **/.hg 4 | **/node_modules 5 | **/.next 6 | **/dist 7 | /lib/lib 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "orta.vscode-jest" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.outputConfig": { 3 | "clearOnRun": "none", 4 | "revealOn": "demand", 5 | "revealWithFocus": "none" 6 | }, 7 | "testing.openTesting": "neverOpen", 8 | "eslint.workingDirectories": ["lib/", "examples/express/", "examples/next/"], 9 | "editor.codeActionsOnSave": { 10 | "source.eslint.fixAll": "explicit" 11 | }, 12 | "editor.formatOnSave": true, 13 | "editor.defaultFormatter": "esbenp.prettier-vscode", 14 | "eslint.validate": [ 15 | "javascript", 16 | "javascriptreact", 17 | "typescript", 18 | "typescriptreact" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim AS base 2 | ENV PNPM_HOME="/pnpm" 3 | ENV PATH="$PNPM_HOME:$PATH" 4 | RUN corepack enable pnpm 5 | 6 | FROM base AS base-deps 7 | WORKDIR /home/node/repo 8 | 9 | COPY ./package.json ./pnpm-*.yaml ./ 10 | COPY ./lib/package.json ./lib/ 11 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 12 | 13 | 14 | 15 | # Express 16 | FROM base AS express-deps 17 | WORKDIR /home/node/repo 18 | COPY --from=base-deps /home/node/repo ./ 19 | 20 | COPY ./examples/express/package.json ./examples/express/ 21 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 22 | 23 | 24 | FROM base AS express-build 25 | WORKDIR /home/node/repo 26 | COPY --from=express-deps /home/node/repo ./ 27 | COPY . /home/node/repo 28 | 29 | RUN pnpm --filter !esi-next run build 30 | RUN pnpm deploy --filter esi-express --prod /home/node/esi-express 31 | 32 | FROM base AS express-prod 33 | COPY --from=express-build /home/node/esi-express /home/node/esi-express 34 | WORKDIR /home/node/esi-express 35 | USER node 36 | EXPOSE 3000 37 | CMD [ "node", "dist/server.js" ] 38 | 39 | 40 | 41 | # Nextjs 42 | FROM base AS next-deps 43 | WORKDIR /home/node/repo 44 | COPY --from=base-deps /home/node/repo ./ 45 | 46 | COPY ./examples/next/package.json ./examples/next/ 47 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 48 | 49 | 50 | FROM base AS next-build 51 | WORKDIR /home/node/repo 52 | COPY --from=next-deps /home/node/repo ./ 53 | COPY . /home/node/repo 54 | 55 | ENV NEXT_TELEMETRY_DISABLED 1 56 | RUN pnpm --filter !esi-express run build 57 | RUN pnpm deploy --filter esi-next --prod /home/node/esi-next 58 | 59 | 60 | FROM base AS next-prod 61 | WORKDIR /home/node/esi-next 62 | 63 | ENV NODE_ENV production 64 | ENV NEXT_TELEMETRY_DISABLED 1 65 | 66 | # Set the correct permission for prerender cache 67 | RUN mkdir .next 68 | RUN chown node:node .next 69 | COPY --from=next-build --chown=node:node /home/node/esi-next/public ./public 70 | COPY --from=next-build --chown=node:node /home/node/esi-next/node_modules ./node_modules 71 | COPY --from=next-build --chown=node:node /home/node/esi-next/.next/standalone ./ 72 | COPY --from=next-build --chown=node:node /home/node/esi-next/dist ./dist 73 | COPY --from=next-build --chown=node:node /home/node/esi-next/.next/static ./.next/static 74 | 75 | USER node 76 | EXPOSE 3000 77 | CMD [ "node", "dist/server.js" ] 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-present Kévin Dunglas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React ESI: Blazing-fast Server-Side Rendering for React and Next.js 2 | 3 |  4 | [](https://coveralls.io/github/dunglas/react-esi?branch=master) 5 | [](https://badge.fury.io/js/react-esi) 6 | [](https://opensource.org/licenses/mit-license.php) 7 | 8 | React ESI is a super powerful cache library for vanilla [React](https://reactjs.org/) and [Next.js](https://nextjs.org/) applications, that can make highly dynamic applications as fast as static sites. 9 | 10 | It provides a straightforward way to boost your application's performance by storing **fragments** of server-side rendered pages in **edge cache servers**. 11 | It means that after the first rendering, fragments of your pages will be served in a few milliseconds by servers close to your end users! 12 | It's a very efficient way to improve the performance and the SEO of your websites and to dramatically reduce both your hosting costs and the energy consumption of these applications. Help the planet, use React ESI! 13 | 14 | Because it is built on top of the [Edge Side Includes (ESI)](https://www.w3.org/TR/esi-lang) W3C specification, 15 | React ESI natively supports most of the well-known cloud cache providers including [Cloudflare Workers](https://blog.cloudflare.com/edge-side-includes-with-cloudflare-workers/), [Akamai](https://www.akamai.com/us/en/support/esi.jsp) and [Fastly](https://docs.fastly.com/guides/performance-tuning/using-edge-side-includes). 16 | Of course, React ESI also supports the open source [Varnish cache server](https://varnish-cache.org/intro/index.html#intro) that you can use in your own infrastructure for free ([configuration example](https://github.com/zeit/next.js/blob/canary/examples/with-react-esi/docker/varnish/default.vcl)). 17 | 18 | Also, React ESI allows the specification of different Time To Live (TTL) per React component and generates the corresponding HTML asynchronously using a secure (signed) URL. 19 | The cache server fetches and stores in the cache all the needed fragments (the HTML corresponding to every React component), builds the final page and sends it to the browser. 20 | React ESI also allows components to (re-)render client-side without any specific configuration. 21 | 22 |  23 | 24 | > Schema from [The Varnish Book](https://info.varnish-software.com/resources/varnish-6-by-example-book) 25 | 26 | **[Discover React ESI in depth with this presentation](https://dunglas.fr/2019/04/react-esi-blazing-fast-ssr/)** 27 | 28 | ## Examples 29 | 30 | - [Next.js, Express, and Varnish](https://github.com/dunglas/react-esi/tree/main/examples/next) 31 | - [React, Express, and Varnish](https://github.com/dunglas/react-esi/tree/main/examples/express) 32 | 33 | ## Install 34 | 35 | Using NPM: 36 | 37 | $ npm install react-esi 38 | 39 | Or using Yarn: 40 | 41 | $ yarn add react-esi 42 | 43 | Or using PNPM: 44 | 45 | $ pnpm add react-esi 46 | 47 | ## Usage 48 | 49 | React ESI provides a convenient [Higher Order Component](https://reactjs.org/docs/higher-order-components.html) that will: 50 | 51 | - replace the wrapped component with an ESI tag server-side (don't worry React ESI also provides the tooling to generate the corresponding fragment); 52 | - render the wrapped component client-side, and feed it with the server-side computed props (if any). 53 | 54 | React ESI automatically calls a `static async` method named `getInitialProps()` to populate the initial props of the component. Server-side, this method can access to the HTTP request and response, for instance, to set the `Cache-Control` header, or some [cache tags](https://api-platform.com/docs/core/performance/#enabling-the-built-in-http-cache-invalidation-system). 55 | 56 | These props returned by `getInitialProps()` will also be injected in the server-side generated HTML (in a ` 24 | 25 |
26 |