Fast Pages with React
Florian Rappl
Posted on June 30, 2022
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;
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;
}
`;
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);
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"
So when a file changes we have:
-
nodemon
detecting the change and restartingts-node
- The output being placed in
dist
-
livereload
detecting a change indist
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"
}
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);
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';
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:
- Build / prepare everything
- 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
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 }}
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.
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;
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}
/>
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.
Posted on June 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.