Introduction
As you may know, I’m a fanatic about speed 🏎️, and recently, my friend Gianmarco Pettenuzzo and I had the opportunity to work on our own blog. Our goal? To build one of the fastest blogs out there! Why? Well, have you ever run a performance tool on one of those “already-made” websites? Most of the time, it’s a bloodbath 🩸.
They don't excel at everything, that's for sure. That's why we're going to build one of the most performant blog on the web ✨. You know that quote from Norman Vincent Peale? "Shoot for the moon. Even if you miss, you'll land among the stars". That's exactly what we're doing here! Shooting for the perfect score, let's see where we'll land.
p.s. you can see the website results in the cover image, the top one is run in desktop mode while the second is for mobile. I too hate getting clickbaited😉.
This post is part 4 of a really long journey. If you missed the first 3 parts, please go ahead and read them first. I'll wait 👇. Part 3: building a high performing static backoffice on AWS with SvelteKit
Welcome back! Here's how this article is structured:
- A simple description of the website and its features;
- The overall AWS serverless infrastructure we used;
- The optimizations we made from a frontend perspective;
- The optimizations we made from a backend/infrastructure perspective.
Backoffice and website features
When building a blog, it's essential to consider both frontend and backend features that will make your site user-friendly, functional, and efficient.
The frontend
The frontend of a blog is what users see and interact with. Here are some key features to consider right from the beginning:
- A visually appealing landing page that catches users' attention and gives them a glimpse of what the blog is about;
- A list of all your blog posts that displays all posts in one place. Each article should have tags so that users can easily find content on specific topics;
- A main page for each blog post that should include a clear title, images, text, and social sharing buttons. You may also want to include a section with related articles to keep users engaged.
The backoffice
The backoffice is where the real creativity begins, and you have complete control over your content. To create a simple yet effective backoffice, you need:
- A login page to ensure that only authorized users can post content, you surely don't want anyone else posting for you 😜;
- A list of all your blog posts, just like the frontend, but it also includes draft posts and allows you to delete blog posts;
- A create/update blog post page where you can work your magic and bring your ideas to life. It must be fast and engaging. We've kept it simple, but we also have plenty of ideas on how to improve it.
AWS Serverless infrastructure
Below, you can find the final infrastructure for our lightning-fast serverless blog built with SvelteKit. As you can see, we have three CDNs: one that only admins can access and is protected by Cognito, one for our public-facing blog, and the last one for static content. Our main CDN is also in front of the API Gateway, which eliminates the need for using CORS (we'll talk about this later in the article).
To manage our static content, we cached everything that was possible to cache, including for the API Gateway. However, this presents a big problem: how can we flush the cache? To solve this issue, we created two Lambda functions:
- One that gets triggered when a new post is updated/created/deleted and flushes the cache for the API layer;
- The other one gets triggered when a new file is uploaded into the S3 bucket and invalidates the content cache.
Caching is a vast topic that deserves its own discussion. To learn more about optimizing content delivery for web apps with CloudFront and S3, check out my previous blog post: Optimizing Content Delivery: The Complete Guide Through S3 Caching and CloudFront
tl;dr: optimizing content delivery for webapp with CloudFront and S3. First a bit of Caching knowledge, followed by real world problems and solutions. In the end I talk about my own website and how I was able to optimize it with some caching strategies.
Frontend optimizations
I'm always struggling with HTML/CSS, and I get the constant feeling that what I'm doing could be done better and in far less time. When it comes to optimizing frontend performance, there are several factors to consider, including network speed, static assets, and best coding practices. Here are some tips that I've learned over the years to optimize my website's performance.
Preconnecting and dns-prefetching
Preconnecting and dns-prefetching can speed up future loads from a given origin by preemptively performing part or all of the handshake. This means that if you're downloading multiple images, you won't have to make multiple DNS lookups; the request will be made only once. To implement this, you can use the following two lines of code (depending on how many URLs you want to prefetch):
<link rel="preconnect" href={PUBLIC_BASE_URL} /> <link rel="dns-prefetch" href={PUBLIC_BASE_URL} />
Prefetching content
Another way to speed up page load time is to prefetch content when users click or hover on links within your website. SvelteKit comes with a built-in keyword for this called
data-sveltekit-preload-data="hover"
. Simply add this keyword in the body attribute within your app.html to enable this feature.
Caching policies
Caching policies are **crucial **for optimizing the performance of a static website and reducing costs. In our website, we implemented caching strategies to cache as much as possible. Here are some of the caching policies we implemented:
- We invalidate CloudFront cache whenever we create or update posts, images, or deploy the web application;
- Static files are cached for one year to reduce the number of requests;
- The only file that is not cached is index.html. SvelteKit, like most frontend frameworks, uses a "burst caching" strategy. This means that when building a new version, the files are named differently, and their names are updated into index.html. If we cache index.html, the browser won't fetch the new content from the CDN until the cache expires.
There is a lot more to say about caching policies, so if you haven't already, please read my previous article on optimizing content delivery with S3 caching and CloudFront. 👇 Optimizing Content Delivery: The Complete Guide Through S3 Caching and CloudFront
Next-Generation HTTP protocol
It's essential to use the latest HTTP protocols, which means deprecating HTTP/1 and using HTTP/2 and HTTP/3. Most browsers and frontend frameworks support these versions, so you just need to check your infrastructure and enable them if they're not enabled by default. In our case, we needed to enable the latest HTTP protocol in CloudFront with the following line of code:
httpVersion: HttpVersion.HTTP2_AND_3
Lazy and eager loading
Lazy and eager loading are important techniques to optimize web page loading times. **Eager **loading means that resources are loaded immediately, while **lazy **loading means that resources are loaded only when they are needed. A combination of both techniques is the key to success. For example, for the blog post list, it is ideal to eager load the first six images and lazy load the remaining six. With pagination set up, this can be achieved easily. For the article itself, it is best to eager load the first image and lazy load the rest, we don't need to rush.
Image Optimization
Images can be a significant bottleneck when it comes to web page loading times. Proper image optimization can significantly improve the performance of the web page. Some techniques to optimize images include:
- Using the next-gen image format such as WEBP, which is supported by 90% of modern browsers.
- Serving images in different sizes, depending on the device being used, to reduce the amount of data being downloaded.
psssss wanna know a secret🤫? Use this tool to compress your images 👉https://squoosh.app/ 👈. It's free and maintained by the Google Web DevRel team.
Lighthouse
Lighthouse is a powerful tool that can be used to evaluate the performance of a web application and provide suggestions on how to improve it. If you have never used it before, be sure to check it out at 👉 https://developers.google.com/web/tools/lighthouse.
Backend optimizations
This one was fun, I went above and beyond to optimize every piece of software I could. Let's start with the first one, my favorite.
Removing CORS
First up, let's talk about Cross-Origin Resource Sharing (CORS). Browsers use CORS to control access to resources in a different domain. When a browser needs to access a resource on a different domain, it sends a "preflight request" before making the actual request. This adds an extra request and slows down the overall response time. To eliminate this bottleneck, we removed CORS altogether. By placing the API layer and the client within the same domain, we were able to save up to 3 times the API response time, bringing it down from ~70ms to ~20ms. Let's see the architecture:
You probably already guess it! To achieve this, we used CloudFront multi-origin configuration. Here's a code snippet that shows how we did it:
As you can see, this is a simple CloudFront distribution. For the complete project, check out the GitHub link at the end of this article. By removing CORS, we were able to significantly improve the performance of our serverless blog.
Caching policies
When it comes to caching, leveraging a CDN is an effective way to improve performance. Since we have already set up CloudFront as our CDN, we can use it as our cache as well. To determine the caching duration, we can set a cache expiration time of one year for our static assets. This means that CloudFront will cache our assets for up to a year, and if we need to update our content, we can invalidate the CloudFront cache and the latest version of the content will be served to the end-users. However, it's important to note that our frontend and backend are both hosted on the same CDN. Therefore, we need to be careful when invalidating the cache. We should only invalidate the cache for the API layer when updating content. For example, if we are updating the content of a blog post, we only need to invalidate the cache for the paths
/api/posts/${id}
and /api/posts/${slug}
. If we are creating or deleting a blog post, we need to invalidate the cache for the paths /api/posts?*
, /api/posts/${id}
, and /api/posts/${slug}
.
Minify code and clean dependencies
Minifying our code and cleaning up unnecessary dependencies is a simple yet effective way to improve the performance of our application. By removing comments, whitespace, and other unnecessary characters, we can significantly reduce the size of our code. Additionally, by ensuring that our packages only include the necessary dependencies, we can further reduce the size of our application and improve its performance. To achieve these improvements, we can use esbuild in our deployment process. For example, if we are using the serverless framework, we can add the following lines to our serverless.ts file:
esbuild: { bundle: true, minify: true, sourcemap: true, exclude: [ '@aws-sdk/client-cloudfront', '@aws-sdk/client-dynamodb', '@aws-sdk/client-s3', '@aws-sdk/lib-dynamodb', '@aws-sdk/s3-request-presigner', '@aws-sdk/util-dynamodb' ], target: 'node18', define: { 'require.resolve': undefined }, platform: 'node', },
In this snippet, the minify option is used to minify our code, and the exclude option is used to exclude unnecessary dependencies from the build process. Since these dependencies are already present in the nodejs18 lambda container, there is no need to include them in our package.
Return only required data
To optimize the performance of your serverless API, it's important to ensure that it returns only the data that is required by the particular page or request. For example, if you're building a list of blog posts, you don't need to include the entire content of each post in the response. This can slow down the page load time, especially if there are a large number of posts. Similarly, when displaying related blog posts, you can exclude the content from the response to speed up the page load time. By returning only the necessary data, you can reduce the amount of data that needs to be transferred and processed, which can significantly improve the performance of your API.
Resize images
Resizing images can have a significant impact on the performance of your serverless application, especially if you're serving a large number of images. One way to resize images is to use the **Sharp **library in a Lambda function triggered by an S3 upload event. This allows you to automatically resize images as they're uploaded to your S3 bucket. You can find more info about it in this blogpost: https://aws.amazon.com/blogs/compute/resize-images-on-the-fly-with-amazon-s3-aws-lambda-and-amazon-api-gateway/. In my case I don't have that time so I will do it by hand with the little secret I've shown you before😊.
Conclusion
In this series, we have explored how to build a lightning-fast serverless blog on AWS with SvelteKit, S3, and CloudFront while eliminating slow CORS. We've looked at how to improve performance from both frontend and backend perspectives. Now, we are getting closer to releasing the website to production and seeing the real-world performance results.
🙏Huge thanks to Gianmarco for helping me out and creating a really cool website, check him out 🙇.
You can find the project here https://github.com/Depaa/website-blog-part4 😉
This series is a journey to production, and we have many more blog posts planned, here the list I'll try to keep updated:
- ✅ Serverless infrastructure on AWS for website;
- ✅ Serverless backend api on AWS for website;
- ✅ Building a high performing static backoffice on AWS with SvelteKit;
- ✅ Building a lightning fast serverless blog on AWS with SvelteKit;
- ✅ Optimizing website analytics, tracking, and SEO;
- Setting up pipelines, monitoring, and alerting for website;
- Deploying website to production;
- Implementing Disaster Recovery strategies while going multiregion with serverless;
- … you see, there are a lot planned, and if you want to add more points just DM me or comment right below.
Thank you so much for reading! 🙏 I will keep posting different AWS architecture from time to time so follow me on LinkedIn 👉 https://www.linkedin.com/in/matteo-depascale/.