Lessons From Six Years of Self-Employment

From 2017 to 2023, I worked as a self-employed web performance consultant. I was lucky to do some cool stuff (work with Google and another company every American knows, help many hot startups, speak at worldwide conferences). In 2024, I turned that page and became an employee at Framer, to lead web performance efforts with a great team of engineers.

Here are some lessons I learned over these six years.

Contents:

Money#

You can probably earn more than you think#

My income in 2017 (when I first started consulting) was $24,000/year – a decent one for a new senior engineer in Belarus. Over six years, that grew to a solid Bay Area engineer’s level.

Every time my income increased, however, I was genuinely surprised: “Wait, I could earn that?”

  • In 2017, when I lived in Belarus and just started consulting, I set my initial hourly price to $30/hr (double what I was making as an employee). A friend of mine convinced me to up it to $90/hr instead. That didn’t work right away, but that allowed me to stabilize it at $60/hr for a couple of years – earning 2× more of what I was expecting to earn initially.
  • In 2021, due to regularly overspending time on audits, I had to bump the price of a web performance audit from $5,000 to $10,000. This effectively doubled my income – and what was completely unexpected for me was that the customers were still happy to pay 2× more for the same audit (because it was still valuable for them)

My lessons from all those bumps that happened over the years were:

  • I can earn more than I think I can;
  • a big reason I’m not earning more is I don’t realize it’s possible.

This “I don’t realize it’s possible” bit is crucial. When you’re a software engineer employed in Belarus, most people around you are also software engineers employed in Belarus. You might see people like you get paid $30,000/yr, $40,000/yr, or sometimes even $60,000/yr. You might see paths to reach that income. But nobody around you earns $200K/yr, so you don’t even realize you could do that. Your own beliefs are your biggest roadblock.

Screenshot of a Threads app thread, linked further below
Another data point for what’s possible. Source

What helped me to earn more was having real, reachable people who pushed me to do that. In 2017, that was my friend Gosha who helped me to become self-employed. In 2021, that was a group of friends who convinced me to try increasing the audit prices. At other moments, it was other folks. If I wasn’t lucky to have all those people, I might’ve still been earning $40,000/yr in Belarus.

I don’t want this bit of advice to be, “well just have good connections lol.” Building good networks is hard, and I’m still struggling with that, having moved to a new country two years ago. However, building a good network can also be an intentional action! Spencer Fry has a great article on how to network more effectively.

How to earn more#

Apart from realizing “I can earn more”, it also helps to know how to do that. If you’re employed, Patrick McKenzie has the best article about salary negotiations I ever saw. If you’re self-employed, Patrick has another article with great advice. And in my case, the bits that worked best were switching to project-based pricing, and investing into a portfolio of cases.

Hourly → project pricing. Hourly pricing is easy to start with, but at some point, it turns from a benefit to a liability. It’s hard to scale – in part because to earn more, you need to put in more hours, but in part because hourly pricing has anchoring. No matter how good of a consultant you are, you’ll have a very hard time invoicing $300/hour – because no regular software engineer costs $300/hour, and because your client is inevitably going to compare your prices to prices of other professionals they encountered.

A solution here is to switch from charging per hour to charging per project. “Project” is a unique deliverable, which means a client can’t easily compare it to anything else, which means that instead of thinking “why is that consultant charging me lawyer rates”, the client will be forced to think in terms of “is this project valuable enough for me”. And this gives you much more leeway! It’s hard to invoice $300/hour, but it’s much easier to invoice $6,000 for a 20hour project if that project helped the client to earn much more than $6,000.

Dan Mall covers how to design project prices in his book.

Building a portfolio. Patrick McKenzie advocates for writing a case study after every successful project. I’m writing too slowly to do that; but still, after every project, I’d normally have a dialog that would go like this:

Me: “Hey, by the way, now that this project is over – could you let me know if you’re satisfied, more than satisfied, almost satisfied, or not really satisfied with the results?”

Client: “Hey – I’m [satisfied / very satisfied]! X was amazing, and Y was great.”

Me: “Oh thank you so much for the feedback! Would you mind if I quote what you said about X and Y for my portfolio?”

This would allow me both to collect feedback about what worked and didn’t work well; and to repurpose that feedback for testimonials. Asking a client to write a testimonial for you is rarely successful – the client has like a thousand more important things to do before that. But taking their feedback, drafting a testimonial based on that, and getting an “okay” from them works much better.

Career#

How to negotiate (money and job titles)#

At my first job, my relationship with money was distant. Sure I wanted to earn more – but saying that to my manager out loud, or actively negotiating a raise somehow felt “dirty”. Great engineers are motivated by challenges, not by money, right? You should focus on growth, and money will follow automatically?

Then, I jumped into consulting and had to learn to sell myself. After doing sales for a while, I’m happy to say my earlier beliefs were very anti-productive.

You vs. company. It is good to be motivated by challenges and focus on growth. But if you don’t also actively negotiate for money, you’ll lose out.

The thing is, your company does negotiate; it’s a business, so negotiating for better conditions – with suppliers, but also with employees – is literally one of its primary activities. Your company is interested in paying you well to retain you – because hiring a new candidate is even more expensive – but if you never negotiate, they’ll keep you on the lower side of “paying well”. Not because they’re bad or hate you or don’t value you enough, but simply because it’s one of their primary activities.

Also, negotiating isn’t “dirty”! It’s a regular activity that everyone in any business relationship is doing. (Remember, your relationship with your company is also a business relationship. And your manager – even if they’re a good friend – is a representative of the company, which means they’ll also negotiate by default.) Nobody thinks of you less when you try to actively negotiate about money. Some people actually think of you more! “Oh, he’s got experience, he’s an equal sparring partner.”

Proof of value. To negotiate about money, it helps to know what value you’re bringing to the company. The way I approach this is by having a “Brag Document”:

A screenshot of the brag document

Every time I do something that results in a clear win, gets praised by somebody else, or exceeds my job responsibilities, I save a link or a screenshot of that into a Notion document. This helps both to build the portfolio (when you’re self-employed), to write self-reviews during quarterly feedback (when you’re employed), and to pick the best examples of your value when you’re approaching your manager for negotiations.

Fear. Negotiations are, of course, very scary. What helps is knowing you have more options – so if your request for more money doesn’t work out, you can go for something else.

In self-employment, having multiple options is natural. If you have a lot of incoming projects, you can freely experiment with prices and negotiate. If you don’t have a lot of projects, and money isn’t good, you can always go back to employment.

In employment, this is harder. But know this: if your company has a good culture, failing at negotiations won’t get you punished. If you ask your manager for a raise, and they refuse the raise, they won’t automatically fire you or think of you worse. Instead, a good manager will either

  • propose a way for you to reach the amount of money you’re asking for (e.g. through a growth plan), or
  • tell you right away that you won’t be able to reach that amount of money at the company.

This means that as long as your company has a good culture, the worst outcome of salary negotiations would be “nothing changes”. Which isn’t that bad! If you don’t try to negotiate, nothing will change anyway.

Write more#

Writing is probably responsible for some of the best outcomes in my career. A webpack performance guide landed me a contract with Google. My web performance blog, Twitter, and Telegram channel were responsible for 2/3rds of the new clients I was getting when consulting. A Twitter thread with web performance tips got me invited to speak at the Smashing Conference.

In general, writing sets you apart from most other engineers. It shows your skills better than an interview. It attracts like-minded people, which increases your career opportunities. The only drawback about it – for me – is that it’s annoyingly slow.

I don’t have great tips for writing, apart from “just try it” (quality comes with practice) and “don’t be afraid that you’ll write badly and be judged” (if one of your blog posts is bad, people will generally just ignore it instead of judging it) and “have some basic form to convert people into subscribers” (otherwise you fail to capture a lot of value from your writing). If you aren’t sure what to write about, think of what you learned this week – and make a short blog post summarizing that. And finally, once you’ve got a grip on writing, see “Making You Writing Work Harder For You” from, again, Patrick McKenzie.

You can pull levers you didn’t know you could#

“Agency” is hard to define, but I’ll try. “Low agency” is when you mostly wait for other people to fix things. “High agency” is when you want something, and you pursue it yourself. Agency is a spectrum, and different people are at different points on that spectrum.

Being self-employed grows your agency a lot. Suddenly, you realize that a lot of things that you always perceived as an unchangeable norm are actually just a result of some human agreements. Which means you don’t have to always follow them! This starts with a simple “I can renegotiate working Mon-Fri” and spreads to deeper “this company process feels annoyingly broken, but I can just try and fix it?” or “if nobody in the company has ever tried shadowing the CEO but I want to, I can just ask.”

See also: “Things You’re Allowed To Do.”

Personal#

Having infinite money might change you for the worse#

In 2021, I had infinite money. I lived in Belarus, where the median salary is $600/mo, I was earning a Bay Area-level income, and I was paying a 3% tax on that income. I spent a lot, but I was still earning faster than I could spend.

That was an interesting experience. I felt rich and important. I tried some luxuries. I also became a snob; I was feeling better than others – and this was noticeable, especially to my friends and family. I ignored their feedback.

Most of these changes, luckily, got reverted when I moved from Belarus to the Netherlands, and my income and taxes got much more aligned with what other people were earning. It took me a few months to notice and undo the personality changes; and a year to get rid of most overly expensive habits.

I’m still trying to figure out what lessons to take from this experience.

My biggest learning so far, I think, is that I can’t always trust my gut to guide me. “Do I feel right about this?” is one of the primary decision-making approaches I use, alongside the regret minimization framework. The challenge is that my gut didn’t feel wrong at all! Neither when I was choosing to act pretentious nor when my friends were telling me I’d changed.

My second biggest learning is that there isn’t that much value in being rich. It’s nice, of course, and if I ever get rich again, I’d probably enjoy it. But I feel I understand both the gains (feelings of prestige, access to nice luxuries, life that’s easier in some aspects) and the costs (more work, fewer people who I can be vulnerable with, perhaps similar personality changes again) much better now. And at this point, I’m not feeling like paying them again.

Burnout is hard to notice#

In 2023, I was actively burning out. I was getting bored of repetitive projects; I was pushing myself to earn more, but to no avail; and I was struggling to make my consulting schedule work with other big obligations I had.

This, luckily, was resolved when I joined Framer; I’m much better now. But what that experience taught me is:

When you’ve been feeling bad for a while (burning out, depressed, or similar), you mostly forget how you felt before. You tell yourself, “Well, I’m feeling a little meh about my work, but I guess it’s always been like this? Like, nothing has really changed recently?” You tell yourself, “A lot of people feel meh about their work, it’s normal and expected.” And you don’t act on this “meh” feeling in any way. This is a trap.

What you actually need to do if you’re feeling “meh” is to look back. Have you been feeling this “meh” for more than six months? Something is definitely off, this isn’t how most people feel at the baseline. Have you been trying to fix that “meh” for a while? Your approach to fixing that obviously isn’t working. Try something else.

What helped me was raising these feelings of “meh” with an experienced manager friend of mine – and, through conversations with her, realizing that I might’ve outgrown the goals I had. Once that happened, it became clear what I needed to change.

Confidence comes from being at peace with yourself#

For the past three years, whenever I’d see any consulting agency site with a heading like this, I’d shrink inside:

A screenshot of a text block starting with “Why is Graphite better than other agencies/the best?”

You could never make me write a paragraph starting with “Here’s why PerfPerfPerf [Ivan’s consulting agency] is the best.” Not because my work wasn’t good – I was regularly delivering 2-4× React performance improvements for my clients – but because the thought of writing this would instantly drown me in anxiety. Am I actually the best consultant? Don’t I suck at X, Y, Z?

This is an issue with confidence. Surprisingly, what I learned about confidence – from my experience and from that of others – is that past achievements barely help with it. You can rely on them to feel more confident, but no matter your number of successes, a single failure is often enough to bring the anxiety back in.

It turns out that what actually helps with confidence is being at peace with yourself, aligning your outer self (how you act and present yourself to others) with your inner self (how you actually feel on the inside). I’m not sure why that works. But what I found is when I’m kinder to myself, when I allow myself to be my genuine self – I feel calmer, more relaxed, and more confident, both in who I am and the decisions I make.

Now, how do you get at peace with yourself? Some people get a lot of help from meditation. Some benefit from mentorship. Some do therapy and take drugs. For me, what seems to help the most is being in a secure, trusting relationship where I feel loved and genuinely accepted for who I am. This is still a bit of a crutch – when I rely on relationships to feel accepted, I’m borrowing that acceptance from the outside instead of actually producing it inside. But as time goes on, I’m getting better and better at the latter as well.

What’s next#

I achieved the best thing a modern human can achieve:

A dialog with ChatGPT. User: ”Who is Ivan Akulov (3perf.com)? Answer from your memory, do not search”. ChatGPT: “Ivan Akulov is a software engineer and consultant known for his expertise in web performance optimization. He runs the website 3perf.com, where he shares insights, tutorials, and case studies on improving the performance of web applications. His work typically involves analyzing web applications to identify bottlenecks and proposing solutions to enhance loading times, interactivity, and overall user experience. Akulov is recognized in the web development community for his deep understanding of modern web technologies and performance best practices.”

Now that I’m at Framer, for the first time in years, I’m going to work not alone – but in a team of incredibly talented engineers, and on projects I’m very motivated about. I’m very excited about this. Let’s see how it goes.

Thank you to Giulio, Jacob, and Stefan for reviewing earlier drafts of this post.

Make your Google Fonts faster – with font-display

Me & Jacob Groß just launched a new tool: a script that makes your Google Fonts faster by adding font-display support!

? Renders your text 1-2 seconds earlier in slow networks
✂️ Just 550 bytes minified and gzipped
? Falls back to regular loading in older browsers

Google Fonts are notorious for not supporting font-display:

Because of that, a lot of people have to either self-host Google Fonts (which is annoying) – or opt out of font-display completely (which hurts performance). Well, no more.

Try the tool and read more about how it works:

? googlefonts.3perf.com

Case study: analyzing the Walmart site performance

Walmart is one of the top USA e-commerce retailers. In 2016, they were the second after Amazon by sales.

In e-commerce, the conversion is directly affected by how fast the site loads. For many e-commerce companies, making the site faster by 1 second increased the conversion 1.05, 1.1, or even 1.2 times. That’s because the slower the site, the more users abandon it before it loads, and the lesser is the conversion.

Unfortunately, the Walmart site is pretty slow. In my tests, the content of the product page becomes visible only at the third second:

In comparison, for Amazon, the content gets visible at 1.4 seconds. The customer sees the product they came for twice faster!

Let’s analyze the Walmart’s site and see how we can improve the performance – and help Walmart earn more! I’ll use the Lumia 635 product page as an example.

Fix the invisible text#

The first issue with the page is that it gets more or less rendered at 2.3s, but the text isn’t visible until 3.0s:

This happens because Walmart uses a custom font, and by default, Chrome and Firefox won’t render the text until the font is loaded. This is how it looks live:

See how the page stays without the text for a second?

(Network throttled with the “Fast 3G” preset in Chrome DevTools)

Browsers delay rendering the text to prevent a flash of unstyled text (FOUT). However, this makes the content invisible for longer – and likely decreases the conversion!

To change this behavior, we can add the font-display: optional rule to the @font-face styles. font-display controls how the custom font is applied. In our case, it tells the browser to just use a fallback font if the custom one is not cached:

/* https://ll-us-i5.wal.co/.../BogleWeb.css */
@font-face {
font-family: "BogleWeb";
/* ... */
font-display: optional;
}

Now, when a customer visits the page for the first time, they will see the text immediately, rendered in a fallback font. The browser will download the custom font in the background and use it for subsequent pages. The current page won’t get the custom font – this prevents the FOUT:

Now the text is visible immediately.

(Network throttled with the “Fast 3G” preset in Chrome DevTools. The CSS file was substituted with Fiddler)

Side note: single-page apps#

With font-display: optional, the font won’t be applied until the user reloads the page. Keep this in mind if you have a single-page app: navigating across routes there won’t make the font active.

Optimize JavaScript#

Another issue is that the page downloads around 2 MBs of gzipped JavaScript. That’s a lot:

JavaScript code is minified, so I’m only able to analyze it on the surface. Here’s what I found.

Use defer for the first bundle#

Most of <script> tags on the page have either the async or the defer attribute. This is good because the browser can render the page not waiting for these scripts to download:

The page has more scripts in different places, so that’s just an example

However, one large file – bundle.3p.min-[hash].js, 112.3 kB gzipped – doesn’t have either of these attributes. If it takes a while to download (e.g., the customer is on a bad connection), the page will stay blank until the script is fully loaded. Not cool!

To be honest, the bad connection could delay any non-deferred script, even the smallest one. So I’d try to defer as many scripts as I can

To solve this, add the defer attribute to this script tag too. As soon as all JavaScript that relies on bundle.3p.min-[hash].js is also deferred (which seems to be the case), the code will keep working fine.

Side note: performance marks#

In the screenshot above, there’s code that likely measures the time the bundle takes executing:

<script>_wml.perf.mark("before-bundle")</script>
<script src="https://ll-us-i5.wal.co/dfw/[hash]/v1/standard_js.bundle.[hash].js" id="bundleJs" defer></script>
<script>_wml.perf.mark("after-bundle")</script>

This code doesn’t work as expected: because of defer, the bundle executes after both of these inline scripts. Just in case somebody from Walmart is reading this.

Load non-important code only when necessary#

Chrome DevTools have the “Coverage” tab that analyzes how much CSS and JS is unused. If we open the tab, reload the page and click around a bit to run the most important JavaScript, we’ll see that around 40-60% of JS still hasn’t executed:

This code likely includes modals, popups and other components that aren’t rendered straight when the customer opens the page. They are a good candidate to be loaded only when actually needed. This might save us a few hundred kBs of JS.

This is how you load components dynamically with React and webpack:

import React from 'react';

class FeedbackButton extends React.Component {
handleButtonClick() {
// ↓ Here, import() will make webpack split FeedbackModal
// into a separate file
// and download it only when import() is called
import('../FeedbackModal/').then(module => {
this.setState({ FeedbackModal: module.default });
});
}

render() {
const FeedbackModal = this.state.FeedbackModal;

return <React.Fragment>
<button onClick={this.handleButtonClick}>
Provide feedback!
</button>
{FeedbackModal && <FeedbackModal />}
</React.Fragment>;
}
};

Don’t serve babel-polyfill in modern browsers#

If we look into standard_js.bundle.[hash].js, we’ll notice that it includes babel-polyfill:

Pretty easy to find by searching for “babel”

babel-polyfill weights 32.9 kB gzipped and takes 170 ms to download on Fast 3G:

By not shipping this polyfill in modern browsers, we could make the page fully interactive 170 ms earlier! And this is fairly easy to do:

  • either use an external service that serves polyfills based on User-Agent, like polyfill.io,
  • or build a second bundle without polyfills and serve it using <script type="module">, like in the Philip Walton’s article.

Don’t load polyfills multiple times#

Another problem is that the Object.assign polyfill is served in 3 files simultaneously:

The polyfill is small on its own, but this might be a sign that more modules are duplicated across the bundles. I’d try looking into that if I had access to sources.

Remove Node.js polyfills#

By default, webpack bundles polyfills for Node.js-specific functions when it sees them used. Theoretically, this is useful: if a library relies on setImmediate or Buffer which are only available in Node.js, it will still work in a browser thanks to the polyfill. In practice, however, I’ve seen the following happen:

// node_modules/random-library/index.js
const func = () => { ... };

if (typeof setImmediate !== 'undefined') {
// ↑ Webpack decides that `setImmediate` is used
// and adds the polyfill
setImmediate(func);
} else {
setTimeout(func, 0);
}

The library is adapted to work in the browser, but because webpack sees that it references setImmediate, it bundles the polyfill.

Node polyfills are small (a few kBs minified), so removing them usually doesn’t make sense. Still, it’s a good candidate to optimize if we were squeezing the last milliseconds from the page. Removing them is super easy (but needs to be tested – what if some code really needs them?):

// webpack.config.js
module.exports = {
node: false,
};

Decrease the render-blocking CSS#

Apart from JS, page rendering is also blocked by CSS. The browser won’t render the page until all CSS (and JS) files are downloaded.

The Walmart page initially depends on two CSS files. In my tests, the largest of them takes even longer to download than the JS bundle – so it blocks rendering even after the script got downloaded and executed:

Notice how the page stays blank (look into “Frames” in the bottom half of the image) until the CSS is fully downloaded

How to solve this? We can go the way Guardian went in 2013:

  1. Find the critical CSS and extract it into a separate file. “Critical” means “The page looks funny without it”.

    Tools like Penthouse or Critical might be useful here. I’d also tune the result manually to exclude content that’s above the fold but is not very important (e.g., header navigation):

    We can show this a couple seconds later in exchange for faster overall rendering
  2. When serving the initial HTML, only load the critical CSS.
  3. Once the page is more or less downloaded (e.g., when the DOMContentLoaded event happens), dynamically add the remaining CSS:
    document.addEventListener('DOMContentLoaded', () => {
    const styles = ['https://i5.walmartimages.com/.../style.css', ...];
    styles.forEach((path) => {
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = path;
    document.head.appendChild(link);
    });
    });
    

If we get this right, we’ll be able to render the page several hundred milliseconds earlier.

Remove duplicated styles#

In total, the Walmart page downloads three CSS files: one with the font definitions (BogleWeb.css) and two with the app styles (standard_css.style.[hash].css and style.[hash].css). The latter two seemed pretty similar, so I removed all the content except selectors and tried to compare the files.

Guess what? There’re 3400 common selectors among these files – and these selectors mostly have common styles! For the perspective, the first file has around 7900 selectors total, and the second one has around 4400:

The grep command is from StackOverflow

That’s a good area to optimize. This won’t affect time to first paint if we decrease the render-blocking CSS properly, but these CSS files will still load faster!

Add a service worker to cache assets#

The Walmart site is not a single-page app. This means that, on different pages, the customer has to download different styles and scripts. This makes every other page load longer, especially if the customer visits the site rarely.

We can improve that by creating a service worker. A service worker is a script that runs in the background even when the site is closed. It can make the app work offline, send notifications, and so on.

With Walmart, we can create a service worker that caches site resources in the background even before the user needs them. There’re multiple ways to do this; the concrete one depends on the Walmart infrastructure. A good example of one approach is available in the GoogleChrome repo.

Side note: notifications#

With service workers, we also get the ability to send notifications to customers! This should be used with caution – or we can annoy them – but this can increase engagement too. Good examples of notifications are “The product you saved for later got a discount” or “John Ford replied to your question about iPhone 8”.

To learn more, see the WebFundamentals’ guide into web push notifications.

Other ideas#

There’s still a room for further optimizations. Here’re some things that might also help – but we need to confirm that on the real app:

  • Using the local storage for caching large dependencies. The local storage seems to be several times faster than the HTTP cache. We might store large dependencies in the local storage to load them quicker:https://twitter.com/iamakulov/status/981950528027611137

    Update: see the Nolan Lawson’s great comment on local storage drawbacks.

  • Improving the time to first byte. Occasionally, the server spends too much time serving static resources. See the long green bars? That’s the time spent waiting for the server:

    These delays are non-deterministic – I’ve seen them pretty often during the analysis, but they keep happening with different resources every time – so this might be a network issue. Still, I’ve noticed them in WebPageTest results too.

  • Enabling Brotli compression. When you download a text resource from a server, the server would usually compress it with GZip and serve the compressed version. The browser will decompress it later, once received. This compression makes the text several times smaller.

    Apart from GZip, there’s also Brotli – a pretty new compression algorithm which compresses text 15-20% better. Right now, all text resources on the Walmart page are compressed with GZip. It makes sense to try Brotli to see if it improves the average download time.

Bonus Increase the product image quality#

That’s kinda related to performance too.

To reduce the size of the images, Walmart compresses them on the server side. The client specifies the image dimensions it expects to receive, and the server sends the appropriate image:

https://i5.walmartimages.com/[hash].jpeg?odnHeight=&odnWidth=&odnBg=

In most cases, this is great. However, for the primary product images, this has a negative effect. When buying an expensive gadget, I often make a final decision by visiting the product page to see the gadget, to imagine how it looks in my hands. But when I come to the Walmart site, I see a low-quality image with compression artifacts:

See yourself on WebArchive

I’d optimize this part for UX instead of performance – and serve images in a better quality. We can still keep the size difference minimal:

  • Try a different encoding algorithm. WebP is 30% smaller than JPEG given the same compression level. MozJPEG is an optimized JPEG encoder that works everywhere and has significantly less compression artifacts.
  • Use progressive images. Usually, during loading, images are rendered top-to-bottom: you see the top part of the image first, and then it fills
  •  

    Use the <picture> tag to stay compatible with different browsers. For example, we could serve WebP for Chrome and JPEG for other browsers:

    <picture>
    <source srcset="https://i5.walmartimages.com/[hash].webp?..." type="image/webp">
    <img src="https://i5.walmartimages.com/[hash].jpeg?...">
    </picture>
    
  • Serve Retina images with <source srcset>. Like this:
    <picture>
    <source
    srcset="https://i5.walmartimages.com/[hash].webp?odnHeight=450&odnWidth=450,
    https://i5.walmartimages.com/[hash].webp?odnHeight=900&odnWidth=900 2x"
    type="image/webp"
    >
    <img
    src="https://i5.walmartimages.com/[hash].jpeg?odnHeight=450&odnWidth=450"
    srcset="https://i5.walmartimages.com/[hash].jpeg?odnHeight=900&odnWidth=900 2x"
    >
    </picture>
    

Summing up#

So, to optimize the product page on the Walmart site, we can:

  • Fix the invisible text with font-display: optional
  • Use defer for the large JavaScript bundle
  • Load non-important code with webpack’s import
  • Remove polyfills in modern browsers
  • Decrease render-blocking CSS
  • Remove duplicated styles
  • Add a service worker for caching assets in background

With these tricks, we can render the product page earlier by at least 400-600 ms. If we apply similar improvements to the whole site, we can increase orders by at least 3–6% – and help Walmart earn more.

Thanks to Jacob Groß, Vladimir Starkov, and Anton Korzunov (in no particular order) for reviewing this post.


Backend for front-end devs: Part 1, Node.js

A few months ago, I had to build a full-stack app having only front-end experience. This is an overview of backend tools and practices if you discover yourself in the same situation.

I’m a front-end dev, but I need to write a full-stack app. What to do?#

To write an app, you’ll have to figure out three things: a programming language for the backend, a database to store data, and infrastructure.

Popular backend languages include Ruby, Python, JavaScript (Node.js), Java and PHP. If you’re a front-end developer, start with Node.js – things would be way easier.

I’ll cover databases and infrastructure in the next parts.

OK, I’ve just installed Node.js. What is it?#

Node.js enables you to write servers in JavaScript. It’s a JavaScript engine similar to the one Google Chrome has. You pass it a JS file, and the engine runs it:

// index.js
console.log('42');
# Shell
$ node ./index.js
42

To launch a real server, you’ll need to call a specific method provided by Node.js (e.g, createServer() from the net module). In practice, a few people do this – almost everyone uses high-level wrappers.

I’ve heard a bit about Node.js and callbacks. What’s the deal with them?#

Callback is a function that you pass into other function. It’s called when a specific action completes. MDN docs

The primary thing you need to know about Node.js is that it performs all long actions (like reading data from a database) asynchronously – using callbacks. This means that to make a DB request, you don’t do this:

const data = sql.query('SELECT name from users');
console.log(data);

but do this instead:

sql.query('SELECT name from users', (error, data) => {
  console.log(data);
});

Under the hood, Node.js sends a request to the database and immediately continues executing the code. And when the request completes, Node.js calls the callback and passes the data to it. This helps to write servers that handle lots of requests at the same time.

Can I write code without callbacks?#

Yes. If you don’t like passing functions into other functions, there’re a couple of alternatives:

  • Option A: use promises instead of callbacks. You’ll still have to pass functions around – but you’ll write code without excessive nesting which often happens with callbacks:

    Use util.promisify to adapt native callback-based APIs to promises
    // With callbacks
    const fs = require('fs');
    
    fs.readFile('./text.txt', (err, data) => {
      console.log(data);
    });
    
    // With promises
    const fs = require('fs');
    const util = require('util');
    const readFile = util.promisify(fs.readFile);
    
    readFile('./text.txt')
      .then((data) => {
        console.log(data);
      })
      .catch((err) => {
        // ...
      });
    
  • Option B: use async/await with promise APIs. This lets you write code that looks synchronous:

    async/await is available with Node.js 7.6+.
    // With callbacks
    const fs = require('fs');
    
    fs.readFile('./text.txt', (err, data) => {
      console.log(data);
    });
    
    // With async/await
    const fs = require('fs');
    const util = require('util');
    const readFile = util.promisify(fs.readFile);
    
    try {
      const data = await readFile('./text.txt')
      console.log(data);
    } catch (err) {
      // ...
    };
    

Many native Node.js APIs (like fs.readFile) also have synchronous alternatives (like fs.readFileSync):

const fs = require('fs');
const data = fs.readFileSync('./text.txt');

However, avoid using them on real servers – or the server would die just from a few simultaneous clients. (These APIs are useful in console scripts though.)

OK, how do I create a server?#

Use Express:

const express = require('express');
const app = express();

// ↓ Define a REST request
app.get('/api/user', (req, res) => {
  res.json({ name: 'Jason Bourne' });
});

// ↓ Ask the server to return statis files
// from the `public` dir (if the /api/user route didn’t match)
app.use('*', express.static('public'));

app.listen(3000, () => {
  console.log('Server is listening on port 3000')
});

Express is useful for building static servers and REST APIs.

How do I build something real?#

In a real app, apart from an HTTP server, you’ll need to implement a few more things. Here’re the common solutions for them:

  • Authorization: Passport.js. Passport.js works with login-password pairs, social networks, OAuth and lots of other login strategies. A SaaS solution like Auth0 is also an option
  • Validating requests: express-validator. express-validator normalizes and validates REST data you recieve from the clients
  • Sending emails: Nodemailer. Nodemailer works with SMTP; it also has simplified settings for AWS Simple Email Service
  • Password hashing: bcrypt. Important: learn how to store and hash passwords properly
  • Logging: loglevel, debug, or winston. I haven’t found a perfectly satisfying solution
  • Running the app in production: PM2. PM2 restarts an app in case of crashes, deploys a new version without a downtime, distributes the load to multiple instances and allows to deploy remotely

That’s it! The next part of the guide will introduce you into databases (specifically, into MySQL).