Building your landing

Julia Suarez
13 min readMay 5, 2021

Building a landing page was one of my 2021 resolutions, however, I usually get caught up working too much on different projects and I never find the time to do things for myself, it’s time to be different, time to do what you promise, and give that step to actually move forward to achieve your goals.

You can see the final result here: https://juliasuarez.dev/ and the source code is available at: https://github.com/JSilversun/js-landing

The first question I had was, what should I use to build it? Since the structure is so generic, maybe using WordPress was the right call since I could find nice templates for 12 — 60$ depending on the one I chose.

I knew that using a pre-made template was going to save me a lot of time, I have some WordPress experience so it would be easy to just go and fill in the information, but in the end, I chose not to do it. Why? To be honest, I’m a prideful person, and as soon as I liked one template I knew I was going to be unhappy with the result.

It’s funny because when you are building software you are always using all kinds of abstractions, nobody really builds anything from scratch, otherwise, we would use ones and zeroes for that matter. Besides nobody usually cares what you use to build a website if it looks good enough, functions properly, and doesn’t have overwhelming performance issues.

However, it’s important to understand your needs, I’m a person who likes to change everything if got a better idea in mind, which means I appreciate customization over initial speed by something prebuilt, you can satisfy this need by using plugins like Elementor, but it comes with paid limitations for advance components.

A final reason to chose using a frontend framework is that I wanted to face my own limitations and figure out what I could come up with, Am I as good as I think I’m? How long will actually take me to build it? How well will it score regarding performance, SEO, and style? I wanted an answer to all of these questions. So, let's get them!

Technology stack

I picked Vue and Vuetify (UI component framework) for the frontend development since these are the ones I have more experience with. For the hosting, I chose firebase since it comes with a great free plan and they had different products like Firestore, Cloud functions, etc just in case I needed them.

So, having this decided, I just needed to figure out a website structure that looked good enough, so I looked for a lot of templates on themeforest and dribbble to gather inspirations, I recommend these sites if you are looking for design ideas.

In about a week I had a website structure I actually liked, so building a landing page wasn’t that complicated, however, it took me like one week to test different ideas for the animations. During this kind of development, it’s easy to wander over things that in the end don’t really matter, for example, most of the early animations I tested never got to the final landing page, however, I did learn a lot.

I came across an interesting animation library called AnimXYZ, they supported Vue and React and after reading their documentation they have a Practical Examples section, I really like them, you can actually see how your application comes to life!

Up to this point, I had an initial version with responsive design and basic animations, the next step was developing the content because it’s not the same to read a bunch of lorem ipsums with placeholders than seeing yourself and what you actually want to say or show, I figured once I had the content I would realize if I wanted to add more sections or modify the existing ones.

Can you guess what part took me more effort? Yes, the hero section! Why? Because I needed skills that I don’t currently have like photography for websites or photo editing, I spent 3 days taking like 200 pictures and hating all of them, the first night I wasn’t able to sleep because this was taking more time than I was expecting and I didn’t see actual progress.

You might take a look at the landing page and say the image is too simple, but usually simple things have a lot of work behind them, the main problem was taking a photo that actually has a nice background for the text, otherwise, it wouldn’t be readable, I didn’t want a complicated image edition over the hero section because this would mean I couldn’t change it regularly.

So, I learned the basics of Lightroom on how to fix lighting issues and then I realize it was easier to be more careful on taking the photo than actually editing it, I was lucky that I have a gray room it went great with the overall tones of the page.

The next step was to buy a domain! Namescheap has great prices, but this was the first time I was actually buying and maintaining a website so I didn’t know if I had to code some script to manage the certificates, fortunately, this wasn’t the case since firebase manage the certificate renewal on their own, so once you buy the domain and link it in the firebase hosting section, you will get nice https protection for free. To learn more about firebase and custom domain read their docs.

Firebase hosting section to set up your custom domain

Optimization

Once I got my domain, content, and structure ready to go, it was time for the last phase, facing the judge, the dragon, lighthouse! I was excited because I was careful of minimizing my dependencies and every time I added a new one I used bundlephobia to check they weren’t a deal-breaker.

I used 3 sites to test the performance of my website:

  1. WebPageTest
  2. PageSpeed Insights
  3. Measure.dev

Mainly WebPageTest and PageSpeed Insights helped me improve different areas, the first test wasn’t good at all.

We can notice how some images really delay the page load, so I started there, how to optimize images? I needed to automate the thumbnail generation and use smaller sizes.

Optimizing images

My first idea was to use firebase cloud functions and firebase storage, something like when I upload an image to firebase store, use a cloud function to generate an optimized thumbnail. It looked simple enough, however, it felt quite manual. I used a library called sharp. This was the main code for the firebase function:

I realized I needed not just the thumbnails but the main image to be optimized as well since several of them are pictures taken from a phone and those are way too heavy for any website.

After each upload, generate a thumbnail and an optimized version of the image with a maximum size, then delete the original image from firebase storage since is not needed.

However, I was coding this without too much care and I end up with a crazy recursion one day:

I laughed a lot that day, fortunately, I was able to take down the firebase function quickly before any charging problem, it was a bug when analyzing the file name to see if it was a thumbnail to avoid generating a thumbnail from the thumbnail.

I read many articles about thumbnail generation, it wasn’t fun having to deal with that, figuring out the right parameters for sharp to get the absolute best image size without jeopardizing the quality.

I even learned that gatsby a framework for React comes with a plugin for automatic thumbnail generation and for once I questioned my Vue decision, I considered alternatives for Vue like Nuxt or Grindome but I didn’t want to add more dependencies or deal with new framework learning curves just to optimize my images. Then I found out about Cloudinary. It was like finding an oasis in the middle of the dessert.

Cloudinary allows you to upload your images like you would do with firebase storage, and then in the image URL you can request an optimized version of your image, they transform your image and deliver it to you on-demand, the first time is considered a transformation and then they cache your image for further deliveries, they work as a CDN too so the image responses and sizes were pretty fast.

To give you an example I had this landing image with a size of 800kb approx.

Now if you request the raw image, you will see a slow network performance:

https://res.cloudinary.com/jsilversun/image/upload/v1618318936/landing/portfolio/landing/landing.png

Now let’s say we want an optimized thumbnail, we would need to change the URL like this:

https://res.cloudinary.com/jsilversun/image/upload/w_500,q_auto,f_auto/landing/portfolio/landing/landing

Now let’s analyze the parameters provided to Cloudinary:

  • w_500: The image’s width must be 500px
  • q_auto: Cloudinary can choose the optimal quality for the image
  • f_auto: Cloudinary can choose the best format delivery for the image, for example, the image originally is a png but they can use a webp format which allows having a fewer quality loss

I didn’t even know this image format existed before I built the landing page, and we don’t even know if new formats will appear in the future that optimizes compression way further, so it was nice to delegate this kind of job to a third party and just focus on building the website I want.

They come with a free plan that was good enough for my use case, I encourage you to visit their prices and services to see if it suits your needs.

Just by using Cloudinary, my lighthouse score improved quickly since the size of my thumbnails was like 5kb and full-size images like 60–100kb.

Optimizing icons

The next step was following Vuetify icon documentation to avoid having to load all material design icons when I just needed like 10 of them.

Just to give you some insights, the @mdi/js package exports all icons as an SVG string, that way you can import the icons you need and use them in the rest of the page.

This is how I set up my icons to avoid having to import them into every component. Just declare your icons in a specific file and use them in the Vuetify instance options:

The icons file should look like this:

Then to use them in your Vuetify component you add the text $mdi-<icon_name_in_kebab_case> like this:

This way you don’t need to make drastic changes to your components in order to only import the ones you need.

Font loading optimization

There are several articles about this matter, this is the best one I found, this StackOverflow question is also useful How to load CSS Asynchronously. Lighthouse complained many times that I should preload the fonts CSS, then from the official developer Mozilla docs I got the following definition:

The preload value of the <link> element's rel attribute lets you declare fetch requests in the HTML's <head>, specifying resources that your page will need very soon, which you want to start loading early in the page lifecycle, before browsers' main rendering machinery kicks in. This ensures they are available earlier and are less likely to block the page's render, improving performance.

There is another great article from a guy that actually performed all kinds of tests over font loading The Fastest Google Fonts.

So I end up with the following font set up:

<!--
- 1. Preemptively warm up the fonts’ origin.
- 2. Initiate a high-priority, asynchronous fetch for the CSS file. Works in most modern browsers.
- 3. Initiate a low-priority, asynchronous fetch that gets applied to the page only after it’s arrived. Works in all browsers with JavaScript enabled.
-->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" as="style" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900&display=swap">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900&display=swap"
as="style" onload="this.rel='stylesheet'" />

With this lighthouse stop complaining about having blocking CSS that needed to be preloaded.

UI Framework limitations

Now it came the ugly part, getting a bad score in page speed insights about things I didn’t entirely control.

No matter what I did I kept having a 70–75 score from lighthouse for mobile devices, however, for desktop, I got nearly a perfect score:

I even ran the tests locally in an incognito window just to confirm and I got the same result, with no clue of what to do I look for issues related to Vuetify since it was the nearest abstraction layer, I already reviewed the benchmarks for Vue and it wasn’t the issue.

Lighthouse score locally for desktop
Lighthouse score locally for mobile

I realized I wasn’t the only one having bad lighthouse scores and that this is a known issue for Vuetify https://github.com/vuetifyjs/vuetify/issues/7265

Apparently, even by just creating a Vuetify app you couldn’t get a score above 80 which was a bummer, because there wasn’t a lot I could do to solve it, but then I remembered a quote from the Clean Code book from Robert C. Martin:

Neither architecture nor clean code insist on perfection, only on honesty and doing the best we can. To err is human; to forgive, divine. […]. We air our dirty laundry. We are honest about the state of our code because code is never perfect. We become more fully human, more worthy of the divine, and closer to that greatness in the details.

I really like Vuetify, and with version 3 coming out I hope they can improve the framework’s performance, I’m not going to lie I was disappointed to realize this, but I decided to keep optimistic and understand that code is something that’s constantly evolving with us and that this kind of issues are more common than I would like them to.

For example, I found a couple of articles about Why you should NOT use Material-UI a popular UI framework for React, or the problem of Ant Design components being too heavy I don’t know if these issues are recent or not, or even if we can compare it to my personal experience building this landing, what I do know is that being a Software Developer requires a lot of endurance, patience, and honesty about what’s the best you can do and to keep working to make things better, and that is something we all share.

I will keep looking for the best tools that help me build better apps, in the meantime I still like Vuetify and I will wait to see how version 3 will perform.

Remove unused code

I thought using bundlephobia to analyze the final size of the library/package was enough to decide if it was worth it or not, but I forgot a valuable lesson, even when you deliver gzipped files you can have bad scores in lighthouse if you have unused javascript or CSS, why? Because gzipping only improves the network transference, the browser will still have to parse everything.

For example, I decided to read the final generated CSS and I realized that even though Vuetify only adds CSS for the components I use, there are several global classes that I never used or some CSS really specific for some components states that I wasn’t going to need like several classes for Right-to-left script used for specific languages or classes for the light theme when I only used the dark theme.

Now tools like tailwindcss make you think about how to optimize your components to strip everything that you don’t use for production, using tools like PostCSS helped me remove the mentioned CSS and I was able to remove about 100kb off the non-gzipped-generated CSS which initially weighted 418kb, which was a lot by just installing a tool and reading your production bundle.

I didn’t mess around with my final javascript bundle besides it being quite huge (407kb unzipped).

From this experience, I learned more about the real cost of dependencies and to be more aware during development to detect which are the bottleneck and what things to consider every time I need to pick a library or even if in some circumstances is better for your to write your own solution.

I will share my final post.config.js file for you to get an idea of how to strip classes from your final CSS.

// postcss.config.js
const
IN_PRODUCTION = process.env.NODE_ENV === "production";
const margins = ["ml", "mr", "mb", "mt", "mx", "my", "ma"];
const paddings = ["pl", "pr", "pb", "pt", "px", "py", "pa"];
const sizes = ["xs", "sm", "md", "lg", "xl"];
const CSS_SELECTORS_TO_REMOVE = [
"rtl",
"picker",
"theme--light",
"order",
"hidden",
"overflow",
...[paddings, ...margins]
.map((item) => sizes.map((size) => `${item}-${size}`))
.flat(),
];

module.exports = {
plugins: [
IN_PRODUCTION &&
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("@fullhuman/postcss-purgecss")({
content: [
`./public/**/*.html`,
`./src/**/*.vue`,
`./node_modules/vuetify/dist/vuetify.css`,
],
blocklist: CSS_SELECTORS_TO_REMOVE.map(
(selector) => new RegExp(`.*${selector}.*`)
),
safelist: [/^router-link(|-exact)-active$/, /data-v-.*/, /.*xyz.*/],
}),
],
};

Conclusion

I hope you have enjoyed and learned something from this article, building something always come with unexpected situations, in the end, we just need to keep learning from whatever we do, to be honest, and keep working to make things better and transform our realities since building Software is kinda like a superpower, use it wisely.

--

--