Next.js でPreact を使う

avatar

Yuji Matsumoto / October 29 2020

https://github.com/developit/nextjs-preact-demo を参考にしながら、このブログの React を Preact で置き換えたので、やったことをメモしていく。

目次

  1. Preact を install する
  2. Webpack の config をする
  3. 置き換えた結果

Preact を install する

まずは Preact を install する。

$ yarn add preact preact-render-to-string

次に、React 周りの alias を設定していく。 npm install を実行する際に、npm install <alias>@npm:<package> のように option を指定することで、任意の alias 名で module を install することができる。 (yarn にも同様の option がある) 今回は、React 関係の module を Preact で置き換えていきたいので、以下のように command を実行する。

$ yarn add react@npm:@preact/compat@^0.0.3 react-dom@npm:@preact/compat@^0.0.3 react-ssr-prepass@npm:preact-ssr-prepass@^1.1.2

Webpack の config をする

https://github.com/developit/nextjs-preact-demo/blob/master/next.config.js からまるっとコピーしてきた。分かる限りで各 config の意味を書いていく。

experimental.modern

  experimental: {
    modern: true,
  },

splitChunks

  webpack(config, { dev, isServer }) {
    const splitChunks = config.optimization && config.optimization.splitChunks
    if (splitChunks) {
      const cacheGroups = splitChunks.cacheGroups;
      const preactModules = /[\\/]node_modules[\\/](preact|preact-render-to-string|preact-context-provider)[\\/]/;
      if (cacheGroups.framework) {
        cacheGroups.preact = Object.assign({}, cacheGroups.framework, {
          test: preactModules
        });
        cacheGroups.commons.name = 'framework';
      }
      else {
        cacheGroups.preact = {
          name: 'commons',
          chunks: 'all',
          test: preactModules
        };
      }
    }

config.optimization.splitChunks.cacheGroups

if (cacheGroups.framework) {
  cacheGroups.preact = Object.assign({}, cacheGroups.framework, {
    test: preactModules,
  });
  cacheGroups.commons.name = 'framework';
}
  • もしも splitChunks の無い version の webpack を使う場合は、cacheGroups.preact を commons chunk に入れる
cacheGroups.preact = {
  name: 'commons',
  chunks: 'all',
  test: preactModules,
};

alias

const aliases = config.resolve.alias || (config.resolve.alias = {});
aliases.react = aliases['react-dom'] = 'preact/compat';
  • react react-dompreact/compat で resolve するように設定している

dev 環境向けの設定

if (dev) {
  if (isServer) {
    // Remove circular `__self` and `__source` props only meant for
    // development. See https://github.com/developit/nextjs-preact-demo/issues/25
    let oldVNodeHook = preact.options.vnode;
    preact.options.vnode = (vnode) => {
      const props = vnode.props;
      if (props != null) {
        if ('__self' in props) props.__self = null;
        if ('__source' in props) props.__source = null;
      }

      if (oldVNodeHook) {
        oldVNodeHook(vnode);
      }
    };
  } else {
    // inject Preact DevTools
    const entry = config.entry;
    config.entry = () =>
      entry().then((entries) => {
        entries['main.js'] = ['preact/debug'].concat(entries['main.js'] || []);
        return entries;
      });
  }
}
  • dev isServer は Next.js から提供されている値

Server-side で compile される場合

if (isServer) {
  // Remove circular `__self` and `__source` props only meant for
  // development. See https://github.com/developit/nextjs-preact-demo/issues/25
  let oldVNodeHook = preact.options.vnode;
  preact.options.vnode = (vnode) => {
    const props = vnode.props;
    if (props != null) {
      if ('__self' in props) props.__self = null;
      if ('__source' in props) props.__source = null;
    }

    if (oldVNodeHook) {
      oldVNodeHook(vnode);
    }
  };
}
  • if (isServer) { /* ... */ } の部分が該当
  • Preact には Option hooks という機能があり、Preact の render の挙動に介入することができる
  • options.vnodeVNode (Preact の Virtual DOM elements) が作成された際に実行される hook
  • dev 環境でのCircular structure in "getInitialProps" を防ぐために、props.__selfprops.__source を null にしているらしい

Client-side で compile される場合

else {
    // inject Preact DevTools
    const entry = config.entry;
    config.entry = () =>
      entry().then((entries) => {
        entries['main.js'] = ['preact/debug'].concat(entries['main.js'] || []);
        return entries;
      });
  }
  • else 以下の部分が該当
  • Preact の devtools を inject している

next.config.js の全体像

元々あった mdx / font 用の設定を併せて、現在のnext.config.js は以下の様になっている。

// next.config.js

const preact = require('preact');
const mdxPrism = require('mdx-prism');
const withMdxEnhanced = require('next-mdx-enhanced');
const withPrefresh = require('@prefresh/next');

const config = {
  experimental: {
    modern: true,
  },

  webpack: (config, { dev, isServer }) => {
    config.resolve.extensions.push('.ttf');
    config.module.rules.push({
      test: /\.ttf/,
      loader: 'url-loader',
    });

    const splitChunks = config.optimization && config.optimization.splitChunks;
    if (splitChunks) {
      const cacheGroups = splitChunks.cacheGroups;
      const preactModules = /[\\/]node_modules[\\/](preact|preact-render-to-string|preact-context-provider)[\\/]/;
      if (cacheGroups.framework) {
        cacheGroups.preact = Object.assign({}, cacheGroups.framework, {
          test: preactModules,
        });
        cacheGroups.commons.name = 'framework';
      } else {
        cacheGroups.preact = {
          name: 'commons',
          chunks: 'all',
          test: preactModules,
        };
      }
    }

    // Install webpack aliases:
    const aliases = config.resolve.alias || (config.resolve.alias = {});
    aliases.react = aliases['react-dom'] = 'preact/compat';

    if (dev) {
      if (isServer) {
        // Remove circular `__self` and `__source` props only meant for
        // development. See https://github.com/developit/nextjs-preact-demo/issues/25
        let oldVNodeHook = preact.options.vnode;
        preact.options.vnode = (vnode) => {
          const props = vnode.props;
          if (props != null) {
            if ('__self' in props) props.__self = null;
            if ('__source' in props) props.__source = null;
          }

          if (oldVNodeHook) {
            oldVNodeHook(vnode);
          }
        };
      } else {
        // inject Preact DevTools
        const entry = config.entry;
        config.entry = () =>
          entry().then((entries) => {
            entries['main.js'] = ['preact/debug'].concat(
              entries['main.js'] || []
            );
            return entries;
          });
      }
    }

    return config;
  },
};

module.exports = withPrefresh(
  withMdxEnhanced({
    fileExtensions: ['mdx'],
    layoutPath: 'src/components/layouts',
    defaultLayout: true,
    remarkPlugins: [
      require('remark-autolink-headings'),
      require('remark-slug'),
      require('remark-code-titles'),
    ],
    rehypePlugins: [mdxPrism],
  })(config)
);

置き換えた結果

  • https://queq1890.info/ の lighthouse の score の比較
    • bundle size が小さくなった分 perf が向上している

React

react

Preact

preact