DEV Community

Cover image for Building an efficient portfolio from scratch.

Building an efficient portfolio from scratch.

joelbonetr profile image JoelBonetR ・Updated on ・17 min read

Hi there!
I just re-built my portfolio with a 100/100 pagespeed and would like to share my experience with you.
Pagespeed/lighthouse score is never fixed, so it's throwing between 97 and 100 depending on Google algorithms on it.

I'l try to cover all workaround i did for this simple task, step by step, and telling you which tools i used, as it could benefit you if you want to create your own portfolio or build a new one.

It's also the stack we use on a big company for developing and deploying landing pages into production on a fast 1-day cycle (of course the CI script is not the same as we use cloud servers such GCP and AWS, but the rest is near 100% the same) so it could be fine for most beginners to learn how some big companies deal with static content on an efficient way.

  1. Wireframing (pen and paper)
  2. Raw data (copies)
  3. Choosing a webhosting.
  4. Setting up your hosting.
  5. Preparing the version control service (Git)
  6. Continuous Integration (CI script)
  7. Choosing a package manager (npm, npx, yarn..)
  8. Choosing a bundler (parcel.js, rollup, webpack...)
  9. Git ignore
  10. CSS Framework
  11. Building the structure (HTML)
  12. Applying Styles (SCSS / CSS)
  13. JavaScript
  14. Deploy to production
  15. Optimization
  16. Footnotes


If you alredy own a portfolio, stop thinking about your old one, search hundreds of different portfolios, bookmark each you like and then keep looking at them and think on what you like most of each.
Then you got fresh ideas that you can draw on a paper.
Sketch a basic structure without adding content, just write shapes and concepts like a square with the label "PICTURE" and another with "TEXT", for example.
When you feel comfortable with a draw congratulations, you validated your own design.
Do this process twice, one for smartphone view and the second for desktop one.

Tools: Pen and Paper


Think a little bit about which information you want or need to add.
If you are some kinda perfectionist, this could motivate you to reach those close goals you need to feel OK with your portfolio content like when you want to say you know SCSS so you'll need to learn SCSS or learn more about that. Everything that leads you to learn something is good.

When you have all information typed in a doc, try to simplify it and think what can you do with this data or what you want to do with it.
Ask yourself: I want to show it all at first sight? Or maybe I prefer to show this specific information inside a modal? or in some tabs, or in an accordion or...?)

Deciding this at this point will makes you feel more comfortable with the results and will prevent you from re-building parts of the portfolio in a loop.

If your data doesn't fit into your wireframe, draw it again with some modifications to ensure (at least a bit) that your data will look nice in your structure.

Tools: Google Docs


Now you know the shape you want for your portfolio and the data you must fit in, it's time for search a home for your future portfolio.
I recommend you a shared hosting as it's the cheapest option and shared hostings -usually- can run hundred static portfolios without issues.
I tried like 40 different ones along the last 10 years, so based on my experience i can tell you for sure that i go for Banahosting.
Its cost is about 4,95 U$D and i'm in love with this features:

  • It comes with C-Panel so it includes the easier control panel to manage your stuff, it also has a terminal -restricted- access
  • Can run Node.js apps on it (also PHP server side programming language, and MySQL databases and obviously HTML, CSS and JS)
  • You can create e-mail accounts with your domain for free.
  • You can generate and add a nice SSL certificate for free so your site will not appear as "insecure".
  • Free DNS Management. Once you bought a hosting, they will generate your instance and spread your domain across their DNS so you'll get online in like 24h.
  • My static sites never fell down in last 5 years I used to have issues with some hostings where my static webpages fell sometimes for "maintenance tasks" or excuses like that.
  • Softaculous included so you can install loads of web apps easy and use it, or simply modify it to learn.

For more info click on the link and go to Web Hosting button on the top menu (if you go please tell Banahosting guys to send me some money).

Tools: a shared hosting

Setting up your hosting.

Once you have your hosting ready you need to set up or search for few things at first.

  • Go to Services -> My Services -> click on your service -> Log in to CPanel.
  • In CPanel, go to FTP accounts and add a new user with the desired password for your brand new domain, with unlimited quota.
  • Go to Let'sEncrypt SSL and generate a new certificate. Then go to SSL/TLS and ensure it's attached to your domain, otherwise attach it.

That's all for the moment, you will be able to play with all features later.

Tools: a banahosting's web hosting's CPanel

Version Control

This could be the most no-brainer part of the project. Go to, create an account, validate it and go to Projects -> Your projects -> New project.

Don't close GitLab yet, we'll need it in the next step.

Git-cli: You can use the terminal / console to pull, commit, push etc to your Git Repo, or you can use a Git-UI. I personally like GitKraken app, which works well on different OS and comes with a nice built-in merging tools and boards, check it out!

Tools: GitLab

Continuous Integration

Remember that we created an FTP account for our new domain and we signed up and in on GitLab, OK so let's link both things.

Copy the script from this article (and read it if you are interested on knowing more). Don't close it, you will need to set the Environment Variables as explained on the same article.

Go to Settings -> CI/CD -> Variables and set the names and values as explained on the link.

On gitlab, master branch, create a new file named .gitlab-ci.yml .
Paste the script on it, save and push it into master branch.
Now GitLab will detect a CI script and will try to run it because you pushed some commit to Master. It must not fail because Environment variables are set so it must perform a success connection to your hosting. (If you didn't understant this, please read the article linked before).

Tools: GitLab

Package Manager

There are many options, you can read information about all of them if you feel in need. I'll put some examples with official site of each:

There are more than 5, so if you want to know more, simply google it like "yarn vs" or "bower vs" and it will show you different results you can check.

For this use case i'll go with Yarn classic .

Sometimes it causes issues at the first sight on Linux due to cmdtest, if you are in trouble with yarn at this point, run the following commands:

sudo apt remove cmdtest
sudo apt remove yarn
curl -sS | sudo apt-key add -
echo "deb stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update  
sudo apt-get install yarn
yarn install

Well, now you got a package manager, let's set it up for the project.

Create a directory where you want, it will be the root dir of your project.
Let's say we choose the following path:


We will create a void index.html on it (you can also set an index.js (or index.html with a script to link your js app) if you will go for a preact, react, svelte.js, angular or vanilla script app, but it's a portfolio so let's make it as light as possible, so i'll go for an index.html file).

Now, with the context of the terminal on it, we will throw the init command:

yarn init

It will ask some questions to generate a package.json file.
Not all questions are required, and you don't even need a yarn init command TBH, you can write a package.json file or copy one from another project.

If you answer all questions it will look something like this:

  "name": "my-awesome-package",
  "version": "1.0.0",
  "description": "The best package you will ever find.",
  "main": "index.html",
  "repository": {
    "url": "",
    "type": "git"
  "author": "Yarn Contributor",
  "license": "MIT"

Tools: yarn


Another choose to make, this one is easy for me, go for Parcel.js as it manages all "automagically" out of the box and does not require configuration. If you feel more comfortable or want to test another one you can go for Rollup or Webpack for example.

Let's get Parcel.js on our project:

If you want to add it global:

yarn global add parcel-bundler

If you want it only for this project:

yarn add parcel-bundler

After adding Parcel.js, we'll need to set up the scripts to serve your project. Open the package.json file and add the following scripts:

  "scripts": {
    "serve": "parcel index.html",
    "build": "parcel build index.html --no-source-maps"

Now, to clarify, our package.json file will look something like:

  "name": "my-awesome-package",
  "version": "1.0.0",
  "description": "The best package you will ever find.",
  "main": "index.html",
  "scripts": {
    "serve": "parcel index.html",
    "build": "parcel build index.html"
  "repository": {
    "url": "",
    "type": "git"
  "author": "Yarn Contributor",
  "license": "MIT"

At this point we've set the bundler properly and we can throw a yarn serve command to serve our project. Parcel will output the URL to see our project, by default it is http://localhost:1234

Notice: if you type something wrong on a scss or js file, the serve can broke up and you'll need to stop it and run it again. No much pain, simply Ctrl + C to the console, arrow up and enter for getting it up again.

If you stuck with "port already in use" error, check the bonus of this article where i wrote the command to release the port and give some advices.

Tools: Parcel.js

.gitignore file

We will need to set up the .gitignore file and add it to the repository to avoid pushing some unwanted files to the version control.

It will look something like this:


This prevents us to push cache files, node_modules and sourcemap files. You can add .ftpquota if your hosting generate this file, .idea if you use a jetbrains IDE and .htaccess if you customize it depending on the environment (local, develop, production...).

At this point i recommend you to run a git command, not related with .gitignore file but useful.

Sometimes we will need to change permissions to a file, or having different permissions on development than we need on production, this will make git to not interpret permission changes as file changes.

git config --global core.fileMode false

The --global flag will make it be the default behavior for the logged in user.

If you don't want to set it global, then you can go for the switch command:

git -c core.fileMode=false diff

Tools: .gitignore

CSS Framework

It is not necessary to pick one, of course, it will depend on your CSS skills and on what are you specialized in.
If you are a Back-End developer, or a Data Scientist, or a DB Architect, or a non-Web Developer etc you may not be familiar with CSS, so if you want to learn it try to use pure CSS or SCSS (as Parcel will compile it automatically into CSS), but if you think that it will not add nothing to your job and prefer doing it in a painless way then, use a framework and preferably import only those parts you need for not adding unwanted and unneeded extra-weight. You can choose Mustard UI, bootstrap or foundation for example.

I'm not using a framework as is, I've created my own "grid system" using CSS Flex, that is part of a light-weight CSS Framework I'm building to build efficient views (i'll share it when finished).

Let's take an excerpt of it:

$col-margin: 16px;
.flex-container {
  width: 100%;
  padding: 0 15px;
.flexrow { 
  display: flex; 
  flex-flow: row wrap;
  margin: 0 calc(#{$col-margin}/2 *-1); 

We've set a basic layout wrapper called flexrow. This is easy to remember and use but a bit innacurate as it can contain or will generate as many rows as needed depending on the following:

[class*='col-'] { 
  margin: calc(#{$col-margin}/2); 
  word-wrap: break-word; 
.col-50  { 
  flex: 0 1 calc(50% - #{$col-margin}); 
  max-width: calc(50% - #{$col-margin}); 

This is a "column" that will fit into 50% width of parent element.
You can generate as many .col-[Number] as you need changing the number inside the CSS calc() functions only.

You can fork this codepen where i copied some parts of that framework I'm building and use it as a playground or a greenfield to build your own stuff.

When using it, as I've all components isolated, I generated a main.scss with @use statements importing only the components I am using so no extra-weight is added.

Tools: CSS Katana UI framework


If you chose to use a framework, then look at the official doc of it for this point.
If you decided to code only what you need by yourself or fork my "Katana-UI" - don't even know if this will be the definitive name - you can take a look at this next steps.

Looking at the SCSS code on the codepen linked in the framework block , we can set different structures, i'll set 3 examples for purpose:

  • Layout with a full-height block and custom background, and a title right in the middle of it.
    <-div class="container-fluid background-1 full-height">
    <-div class="container">
    <-div class="flexrow">
      <-div class="col-100">
        <-h1 class="d-flex flex-center-x flex-center-y"> my portfolio <-/h1>
  • layout with different background and two content blocks inside, each at 50% of available width. First one with an image and second one with text:
    <-div class="container-fluid background-default">
    <-div class="container">
    <-div class="flexrow">
      <-div class="col-50">
        <-img src="my-image.jpg" class="img-responsive">
      <-div class="col-50">
        <-p> Lorem ipsum dolor sit amet 
  • layout with different column widths, first with text, second with image:
    <-div class="container-fluid background-1">
    <-div class="container">
    <-div class="flexrow">
      <-div class="col-70">
        <-p> Lorem ipsum dolor sit amet 
      <-div class="col-30">
        <-img src="my-image.jpg" class="img-responsive">


If you choose a framework make sure you only @use this scss components you'll need. Loading an entire framework and using only a part is something that we don't want on an efficient view. Let's explain how to deal with it after the next point.
Theming: You will like to set your typos, colors, shadows and more overriding framework defaults or adding new styles to your html elements.

So we will add a theme.scss, where to import the parts we are using of the framework and where to set our theming styles. For example:

@use 'vars/breakpoints';
@use 'vars/colors';
@use 'vars/fonts';
@use 'base/columns';
@use 'base/containers';

.content-block {
  margin-bottom: 25px;
#portfolio-header {
  position: relative;
} {

  h1, h2 {
    font-family: "Ubuntu Mono", Consolas, Monaco, "Andale Mono", monospace;
  h1 {
   display: flex;
    @media only screen and (max-width: 460px) {
      max-width: fit-content;
    span#title {
      font-size: 1.5em;
      font-weight: bold;
      @media only screen and (max-width: breakpoints.$screen-xs){
        font-size: 1.25em;
    span#cursor1 {
      animation: blink .75s linear infinite alternate;
      font-size: 1.5em;
      font-weight: lighter;

To clarify, if you are using CSS you may use @import, which happens at runtime, if you use SASS/SCSS (recommended) @import runs at build time, giving a single CSS file as output, which is preferred (less requests), but we are not using @import statement for the following reasons:

  • @import makes all variables, mixins, and functions globally accessible. This makes it very difficult for people (or tools) to tell where anything is defined.
  • Because everything’s global, libraries must prefix to all their members to avoid naming collisions.
  • @extend rules are also global, which makes it difficult to predict which style rules will be extended.
  • Each stylesheet is executed and its CSS emitted every time it’s @imported, which increases compilation time and produces bloated output.
  • There was no way to define private members or placeholder selectors that were inaccessible to downstream stylesheets.

The new module system and the @use rule address all these problems.

The @use rule loads mixins, functions, and variables from other Sass stylesheets, and combines CSS from multiple stylesheets together. Stylesheets loaded by @use are called "modules".

Of course you can also define _partials to @use it on your main theme.scss file.

Let's remark that we prefer using CSS over JS for reaching styling and interaction components. Interaction? Yes, CSS gives us some features sometimes difficult to implement, but there come SASS to the rescue (and simplifying the task that of course you can reach with plain CSS).
You can create CSS Only modal windows, accordions / collapsible elements, tabs and more using different approaches.

The most common ways to reach this interactive components are falling into pseudo-selectors world, specially :checked and :target.

Tools: SCSS


I said efficient on the title so I'll keep my promise. Using javascript only when strictly necessary will help us to reach our goal.

While CSS only needs to be downloaded and executed, javascipt falls into another path that I'm not going to describe here in deep, instead i'll address you to this detailed process from downloading to execution part 1 , part 2 and part 3 .

I've to confess that i only make a flash read to these articles in an attempt to find some keywords only to check if it could be useful for you and I've already pinned it to my comprehensive read as I think i could learn something new from it, so there's a tl;dr by me to you about javascript stages on web:

Script Evaluation
Script Parsing & Compilation
Execution and Garbage Collection

As you can see there's much more work than the two step downloading and rendering of css.

On my portfolio, I only set a script for the typing-like effect and external Analytics script (async-ed and deferred with GTAG) for an asynchronous loading and deferred to control options script runs after gtag is loaded.

Of course, JavaScript only is preferred to jQuery or any other library that overloads our view on production.

Tools: JavaScript

Deploy to production

At this point we'll have a nice static portfolio and we only need to delete the dist/ folder content to clean it up, throw a yarn build command to "build for production" and upload it to GIT as first version.

If you already set the CI script make sure you only send the dist/ folder content to production. You don't need all base sources on production, and all this resources are not being used even if you upload them after all.

If you didn't set a CI script and you want to make it "the old way", simply perform an FTP upload of your dist/ folder to the root dir of your domain and you're done.

You may want to upload and test your portfolio "as is" with pagespeed and lighthouse or deploying it after the next step.

Tools: CI script on GitLab


Depending on your workaround you can get different warnings from pagespeed and lighthouse.

Note that they are using an emulator with a crawler and monitoring tools that could be less accurate than you may think. I saw with my two eyes (at the same time) how pagespeed shows different scores for the same webpage on a 5 minute difference with a 20 score difference.
This shouldn't happen to you as much but on dynamic webpages with loads of content and modules. On static webpages the maximum difference I've saw it's about 3-5 points as much.

I recommend you to do a pair of things to benefit from the best practices and, if you get a different issue, you can ask me on the comments and I'll answer you with possible workaround to solve your issue.

  • Lighthouse uses pagespeed implicitly so I'll take care from lighthouse only.
  1. CROP OR REDUCE ALL IMAGES. You can use the Chrome emulator to check different screen resolution/DPI and see the maximum shown size of an image (on the "computed" tab from chrome dev tools). So if an image shows at 500px * 300px as maximum, there's no point on keeping it higher and lighthouse will blame you for that.
  2. IMAGE and VIDEO COMPRESSION. Multimedia resources are needed on any layout you can imagine to enrich your visitor's perception and increase the user engagement, which is an important point on SEO, but when this resources are too heavy the user experience decreases as load times do, which is bad for SEO. What to do? Compress your videos and images and give options!! check the PICTURE and VIDEO HTML tags and learn how to provide next-gen formats with a fallback for old browsers.


You will need to iterate over some of this steps in the future, web pages without updates looses SEO positioning and gets boring for the users. This last point must not be applicable to your portfolio (It's a portfolio after all) but if SEO is important to you, add content on a regular basis.

Remember to check lighthouse metrics from time to time to ensure you keep at 100 on the "95th percentile" base that Google uses to estimate our score. It's possible that, at the point you read this, the big G updated something and that my own portfolio lowered some points on some metric.

I also recommend you to register your portfolio property on google webmaster tools (now called Google search console), adding a sitemap.xml and checking usability issues.


I'm writing some posts about CSS only components that are usually coded with javascript. As you read this post about building an efficient portfolio I hope you find this posts interesting.

Here's the Building CSS interactive components from scratch - Part I and the Part II

Hope you find this little bible useful. I would appreciate your impressions on the comments section too, thanks for reading.

Discussion (16)

sakibulislam profile image
Sakibul Islam

@joelbonetr Can you please provide the url of your portfolio? It would be really helpful! Thanks in advance πŸ‘

joelbonetr profile image
JoelBonetR Author • Edited

It's on my profile and linked on the article, moreover on the 100/100 pagespeed link at the top second line of text you will see it =)
Here it is anyway:

I'm editing it those days for Google metrics testing purposes, you may see changes from a day to another.

sakibulislam profile image
Sakibul Islam

Oh my! How did I missed it! Anyways amazing website! @joelbonetr Loved your 20 things about me and Working with section it has a unique aesthetic.

Thread Thread
joelbonetr profile image
JoelBonetR Author

Haha no problem and thanks!
I'll try different approaches for different content blocks in a near future till i find what i like more, hope you for being able to build your own with this post :)

Thread Thread
sakibulislam profile image
Sakibul Islam

Yes I am working on my own portfolio website. I think I can borrow some of your ideas on my website. Is that okay with you?

Thread Thread
joelbonetr profile image
JoelBonetR Author

Yes of course, you can pick whatever you want, but remember that than only copying you'll learn more or less how another one did something, but trying to create your own approach of an idea could make your job better than the source where you find it :)

Thread Thread
sakibulislam profile image
Sakibul Islam

Exactly! I got your points. Thanks for the enlightment πŸ‘πŸ˜Š

Thread Thread
joelbonetr profile image
JoelBonetR Author

You're welcome, don't forget to show us up your work when finished! 😁

Thread Thread
sakibulislam profile image
Sakibul Islam

Yes! Definitely I will show up to it! πŸ˜‰πŸ˜‰

buinauskas profile image

It's on his profile πŸ˜‰

sakibulislam profile image
Sakibul Islam

Oh my! I saw it now! Thanks @Evaldas πŸ‘

nikolakovacevic profile image
Nikola Kovacevic

Hey @joelbonetr ,

as I started to work on my portfolio I was curious about what you did.
I went to check your portfolio, but unfortunately, it is broken on Mac / Firefox

I hope you fix it soon as otherwise, it looks good.

joelbonetr profile image
JoelBonetR Author

Yes, I always code for chrome first, then I'll need to adapt it (some prefixes on CSS rules and so).
Check it on Chrome and you'll be able to see it and inspect it, i'll fix FF, Edge and Safari by next week :)

joelbonetr profile image
JoelBonetR Author

Already fixed SVG render for FireFox and Edge, hope it fix Safari too at the same time, tell me if you see some glitch. Have a nice day :D

dkalomoiris profile image
Dimitris Kalomoiris • Edited

Nice post. Thank you for sharing.
Quick question tho. About-option in your navbar toggler, isn't suppose to redirect you on the About This Site/About Me section? Seems like broken to me. Or it just works like a homepage?

joelbonetr profile image
JoelBonetR Author

Yes it is, thanks a lot for reporting!
I'm too busy those days i'll fix it when possible (portfolio is not my main priority). BTW i would like to refactor some parts and modify some behaviors on different devices too, apart from taking in mind the latest lighthouse updates for getting a 4x 100 score again :D

Forem Open with the Forem app