Fast Pages with React

Florian Rappl - Jun 30 '22 - - Dev Community

Photo by Kolleen Gladden on Unsplash

I recently created the website for my book "The Art of Micro Frontends". For this page I took a rather conservative approach - making a "true" single page (i.e., landing page) that should be as approachable and fast as possible - without sacrificing developer experience.

Surely, there are right now quite some frameworks and tools out there. But I did not want to spent countless hours learning new stuff just to be blocked by some framework restrictions. Instead, I've chosen an approach that - in my opinion - is quite convenient, super fast, and very lightweight.

The Tech Stack

I've chosen to use react as library for writing reusable components. In a nutshell, for the page it allows me to have code like the following:

function Content() {
  return (
    <>
      <Header />
      <Grid>
        <Book />
        <Author />
        <Buy />
        <Outline />
        <Reviews />
        <Articles />
        <Examples />
        <Shops />
        <Talks />
        <Videos />
        <Links />
      </Grid>
      <Footer />
    </>
  );
}

export default Content;
Enter fullscreen mode Exit fullscreen mode

This is very easy to write, change, and align. As far as styling is concerned I've installed styled-components. This allows me to have the CSS next to the component where it should be applied. In a nutshell, this makes writing reliable CSS very easy. Also, when I omit (or even throw out) components in the future their CSS won't be part of the output.

For instance, the Grid component shown above is defined like:

const Grid = styled.div`
  display: grid;
  grid-column-gap: 1.5rem;
  grid-gap: 1.5rem;
  grid-row-gap: 0.5rem;

  @media only screen and (max-width: 999px) {
    grid-template-areas:
      'book'
      'buy'
      'outline'
      'author'
      'reviews'
      'articles'
      'talks'
      'videos'
      'examples'
      'shops'
      'links';
  }

  @media only screen and (min-width: 1000px) {
    grid-template-areas:
      'book       author'
      'buy           buy'
      'outline   outline'
      'reviews   reviews'
      'articles   videos'
      'articles examples'
      'articles    shops'
      'talks       links';
    grid-template-columns: 1fr 1fr;
  }
`;
Enter fullscreen mode Exit fullscreen mode

Theoretically, the grid layout could also be computed via JavaScript - just giving the parts that are included (which is another reason why the CSS-in-JS approach is great here). For now, I am happy with the hard-wired layout.

Personally, I always like to have an additional set of checks for my applications, which is why I use the whole thing with TypeScript. TypeScript can also handle JSX quite well, so there is no need for anything else to process the angle brackets.

Dev Setup

For the whole mechanism to work I use a custom build script. The file src/build.tsx essentially boils down to this:

const root = resolve(__dirname, '..');
const dist = resolve(root, 'dist');
const sheet = new ServerStyleSheet();
const body = renderToStaticMarkup(sheet.collectStyles(<Page />));
const dev = process.env.NODE_ENV === 'debug' ? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1"></' + 'script>')</script>` : '';

const html = `<!DOCTYPE html>
<html lang="en">
<head>
  ...
  ${sheet.getStyleTags()}
</head>
<body>${body}${dev}</body>
</html>
`;

sheet.seal();

addAssets(resolve(__dirname, 'static'));

addAsset(Buffer.from(html, 'utf-8'), 'index.html');

writeAssets(dist);
Enter fullscreen mode Exit fullscreen mode

Most importantly, the collectStyles from styled-components create the inline stylesheet we'd like to use for this page. The dev variable keeps a small refresh script that will only be part of the page during local development.

For running the build.tsx file we use ts-node. By calling ts-node src/build.tsx we can start the process. A few other tools that are helpful for making this a great experience are:

  • LiveServer for reloading during development (i.e., the script above already uses that)
  • Nodemon for detecting changes during development (i.e., once we touch a file the ts-node process should restart)
  • HttpServer for running a local webserver during development (i.e., we need to serve the page from somewhere - http-server dist is good enough for us)

All these tools can be wired together via concurrently:

concurrently "livereload dist" "http-server dist" "nodemon"
Enter fullscreen mode Exit fullscreen mode

So when a file changes we have:

  1. nodemon detecting the change and restarting ts-node
  2. The output being placed in dist
  3. livereload detecting a change in dist and updating the parts that changed

The whole thing is served from http-server. The configuration for nodemon looks as follows:

{
  "watch": ["src"],
  "ext": "ts,tsx,json,png,jpg",
  "ignore": ["src/**/*.test.tsx?"],
  "exec": "NODE_ENV=debug ts-node ./src/build.tsx"
}
Enter fullscreen mode Exit fullscreen mode

One last remark on the dev setup; for getting the assets in a set of custom Node.js module handlers is used:

function installExtension(ext: string) {
  require.extensions[ext] = (module, filename) => {
    const content = readFileSync(filename);
    const value = createHash('sha1').update(content);
    const hash = value.digest('hex').substring(0, 6);
    const name = basename(filename).replace(ext, `.${hash}${ext}`);
    assets.push([content, name]);
    module.exports.default = name;
  };
}

extensions.forEach(installExtension);
Enter fullscreen mode Exit fullscreen mode

Each asset will be added to a collection of assets and copied over to the dist folder. The asset is also represented as a module with a default export in Node.js. This way, we can write code like:

import frontPng from '../assets/front-small.png';
import frontWebp from '../assets/front-small.webp';
Enter fullscreen mode Exit fullscreen mode

without even thinking about it. The assets are all properly hashed and handled by Node.js. No bundler required.

CI/CD

For deploying the page I use GitHub actions. That is quite convenient as the repository is hosted anyway on GitHub.

The whole workflow is placed in the .github/workflows/node.js.yml file. There are two important steps here:

  1. Build / prepare everything
  2. Publish everything (right branch is gh-pages)

For the first step we use:

- name: Build Website
  run: |
    npm run build
    echo "microfrontends.art" > dist/CNAME
    cp dist/index.html dist/404.html
Enter fullscreen mode Exit fullscreen mode

which automatically prepares the custom domain using the special CNAME file. All the output is placed in the dist folder. This will be then pushed to the gh-pages branch.

Likewise, I decided to make a copy of index.html with the 404.html file. This file will be served if a user goes to a page that is not there. Such a mechanism is crucial for most SPAs - in this case we'd not really need it, but it's better than the standard GitHub 404 page.

The second step then pushes everything to the gh-pages branch. For this you can use the gh-pages tool.

- name: Deploy Website
  run: |
    git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
    npx gh-pages -d "dist" -u "github-actions-bot <support+actions@github.com>"
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

Importantly, you need to specify the GITHUB_TOKEN environment variable. This way, the command can actually push code.

Now that's everything for the pipeline - the page can go live and be updated with every push that I make.

Performance

So how does this little page perform? Turns out - quite well. You can go to web.dev/measure to check for yourself.

Performance measured on web.dev

To get 100 in each column also some tricks need to be applied. For instance, instead of just using something like an img tag you should use picture with multiple sources. That was another reason why choosing react was quite good:

interface ImageProps {
  source: string;
  fallback: string;
  alt?: string;
  width?: number;
  height?: number;
}

function getType(file: string) {
  return `image/${file.substring(file.lastIndexOf('.') + 1)}`;
}

function Image({ source, fallback, alt, width, height }: ImageProps) {
  return (
    <picture>
      <source srcSet={source} type={getType(source)} />
      <source srcSet={fallback} type={getType(fallback)} />
      <img src={fallback} alt={alt} width={width} height={height} />
    </picture>
  );
}

export default Image;
Enter fullscreen mode Exit fullscreen mode

With this little component we can write code like

<Image
  source={frontWebp}
  fallback={frontPng}
  alt="The Art of Micro Frontends Book Cover"
  width={250}
  height={371}
/>
Enter fullscreen mode Exit fullscreen mode

which will be applied just as mentioned. Also, quite importantly we specify the width and height of the image. In theory, we could also compute that on the fly when rendering - but as the page only has 3 images it really was not worth the effort.

Conclusion

Writing simple sites does not need to be complicated. You don't need to learn a lot of new stuff. Actually, what is there already will be sufficient most of the time.

The page I've shown easily gets the best score and performance - after all its the most minimal package delivered with - for what it does - the optimal dev experience.

The code for the page can be found on GitHub.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .