React & AMP: Modern Approach
Dilemma 🤔
AMP has not gotten a great response overall, but I’m not going to talk about that a lot here. If you’re just thinking about AMP, you’ll probably want to read the start and end of this article. If you’re already committed but have yet to begin, the whole article will act a pseudo-guide to get you there.
I approached AMP as an challenge: build a mirrored UI that follows strict(er) requirements and delivers a great UX, but also seamlessly integrates and operates alongside our existing infrastructure.
If we can integrate AMP into our app with minimal effort and potentially gain more traffic or better conversions, then why not? This kind of thinking is probably what leads to the continued existence of AMP… but lets forget about that dilemma for now.
An Incremental Approach 📶
The idea of you building only an AMP version of a website is kind of ridiculous. I am assuming you already have a React app and you are happy. You can opt-in to AMP on a page-by-page basis, and that’s exactly what you should do.
Most importantly, make sure to measure exactly how AMP performs while stepping into the waters slowly. To start, pick a page that’s relatively static, with minimal fancy-ness, but also gets a decent amount of traffic. Landing, or some sort of content pages, are great candidates.
Primary Requirements 📃
Lets focus on the key things required to get a page AMPed, how we can do it with React, and what the best case scenario looks like.
Static ❄️
An AMP page must be static and cannot load any of your regular JavaScript bundles. Google must be able to fetch all the HTML from a URL you designate as the AMP version of a page.
Best case: You’re server-side rendering your app. This is a very easy transition. We’re simply going to switch from ReactDOMServer.renderToString(reactTree)
to ReactDOMServer.renderToStaticMarkup(reactTree)
. Since we’re not going to be hydrating React on the client, we can just render the static markup only (without all the react IDs) and save on the overall HTML size. If you already have an isomorphic app, you’re likely already familiar with making sure your server renders match the client ones, so things should just work!
You’ll also need to make a few changes to your new HTML tree, such as using <html ⚡️ lang="en">
and so on, but those are fairly small mark-up changes. Oh and don’t forget to strip away any script tags of your JS bundles.
Worse case: If you’re only doing client-side rendering, you’ll need to pre-render your pages so that the HTML can be served fully-baked for AMP. There are tools like react-snap and services like prerender.io to help you get there.
Inline and Small CSS 💅
With AMP, your CSS must be small in size and be inlined in the HTML (not to be confused with inlined per element).
Best case: You’re using a library like styled-components, where your CSS is cherry-picked based on what is rendered on the page. That means you’ll never have pages with unused CSS and it will generally be small. In addition, styled-components inlines CSS by default, so that problem is solved as well. If this sounds like a plug for styled-components, it totally is.
Worst case: If you’re using traditional style sheets, you’re going to either cherry-pick the CSS manually (insane) or utilize your build or runtime tools to do it for you.
No Bad CSS ⛔️
There are certain things you cannot use in CSS on AMP pages. Perhaps the most important one is !important
. All the others such as some CSS transitions can be handled on an ad-hoc basis.
Best case: You are using a CSS solution that namespaces everything to be local to components (e.g. styled-components), meaning that you’ll never be using important!
in the first place.
Worse case: This is your chance to clean up your CSS. important!
is just bad practice.
AMP Specific Components for Interactivity 🔨
Lets say you have a Facebook share button on your page. This button has a click handler that uses the Facebook API to trigger a share dialog. Interactive elements like this that were driven by your JS simply won’t do. If you’re not loading your JS, this button is not functional.
So, you can either remove that component on AMP renders, or you can substitute it with something from the AMP library. It’s generally better to find a substitution because AMP pages should closely mirror your real pages. In either case, you need to introduce some sort of branching between AMP and non-AMP, and ideally you do this in a declarative way.
Consider building a React hook useAmp
that has an API similar to this: const isAmp = useAmp()
. How you implement that hook is up to you. You could hook it up to detect the presence of amp in your URL, or use React’s Context API.
Going back to our requirement of ‘declarative’ branching, we can create two components Amp
and NotAmp
. Here’s how the first could look like (the other would be similar):
export default function Amp({ children }) {
const isAmp = useAmp() if (isAmp) return children
return null
}
In the example of a Facebook button, we can now do something like this:
<NotAmp>
<OurFacebookShare .../>
<NotAmp>
<Amp>
<amp-social-share type='facebook-share' .../>
</Amp>
The key takeaway here is that you shouldn’t have to create a whole new React tree to branch off between AMP vs. non-AMP pages. Instead try to introduce branching at component level to be able to satisfy both requirements, and at the same time you’ll have very similar looking versions of the page. If you picked a good candidate page to AMPify, you’ll find that this branching won’t be happening too much.
Images
This is perhaps the most frustrating requirement and is closely related to what we talked about above. AMP forces you to use their component in declaring and loading images (it handles things like lazy loading to further optimize the page load). Images are obviously very common, so while you can avoid things like the Facebook share button, you’re not going to be able to avoid this.
You can improve on the API introduced earlier by introducing something I call AmpSafeWrapper
components. We can write an HOC like this:
export default function withAmpSafeWrapper(AmpComponent) {
function WithAmpSafeWrapperComponent(props) {
const isAmp = useAmp() if (isAmp) return <AmpComponent {...props} />
return props.children
} return hoistStatics(WithAmpSafeWrapperComponent, AmpComponent)
}
We can then use this HOC around our AMP components to expose a wrapper:
export default function AmpImg({ src, height, width, layout }) {
return <amp-img
src={absoluteURL(src)}
height={height}
width={width}
layout={layout}
/>}export const AmpImgWrapper = withAmpSafeWrapper(AmpImg)AmpImg.propTypes = {
src: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
layout: PropTypes.oneOf(LAYOUTS),
height: PropTypes.number,
}
And finally, we can utilize it whenever we have an image:
const imageProps = { src: ..., height: ..., width: ... }<AmpImgWrapper {...imageProps}>
<img {...imageProps}/>
</AmpImgWrapper>
It’s still a bit verbose and repetitive because we need to pass the props multiple times, but you get the general idea.
Final Words 👋
Fundamentally AMP is pushing towards a faster and arguably a more polished web experience. But breaking it down piece-by-piece, what AMP really turns out to be is just a CDN cached lean static website, that lazy loads different resources on the page. AMP’s techniques can be fully replicated without the framework.
It’s pushing us to do better. Stop the bloat and focus on the UX. It gives us the tool kit to get there.
It’s also pushing us to do all that through methods that don’t really need to be different from what we already have available to us.