Quick tips for a better website

These are some tips to improve your website, examples are for Jekyll as my website is based on it but the main tips are technology agnostic.
1) Use Cloudflare
-
Enable all useful page speed and performance options in Cloudflare panel:
Speed -> Settings: (In the Site Recommendations section): Enable/Disable according to the picture:
Enable: HTTP/2, HTTP/3, HTTP/2 to Origin, Always use HTTPS, TLS 1.3, Early Hints
Disable: All the rest, including 0-RTT Connection Resumption (this option can speed up the connection but it also makes it less secure by providing grounds for replay attacks so we disable it).
Another less noticed but still very important tip:
-
Enable TLS 1.2 in addition to TLS 1.3:
I was able to retrieve my website feeds again in Newsflow2 and some other RSS feeds clients after enabling TLS 1.2 again, these clients donāt show any errors but still donāt load your feeds if youāve only enabled TLS 1.3.
Use this setting in Cloudflare, SSL/TLS -> Edge Certificates:
Minimum TLS version: TLS 1.2
How I found it was TLS issue? I used RSS Validator3 for checking the validity of my website RSS feed after some changes and it returned TLS error, so I got suspicious that it might be due to only TLS 1.3 being enabled and I tested after enabling TLS 1.2 and voila! My RSS feed clients started working again!
-
Use Cloudflareās Email Address Obfuscation for your email address but use it only on one Contact page:
Remove email address from website footer and only use it in a Contact page to remove the overhead of Cloudflare JS used for email address obfuscation to only one page.
A more solid solution is to use a local JS for email address obfuscation, the local solution can be more complicated but still suffers from lack of randomization. The Cloudflare email obfuscation solution is good enough, enable it if you havenāt already.
To enable Cloudflare email obfuscation:
Scrape Shield -> Email Address Obfuscation
While there you can also enable Hotlink Protection.
2) Reduce Cumulative Layout Shift(CLS)
-
Use hardcoded width and height for
img
andvideo
tags. -
Use a CSS class to still support responsive after doing no. 1.
This is the way I handle it in my Jekyll website, in
base.scss
file:// Fix Layout Shift issue(combined with adding width and height to html img and video tags) img, video { max-width: 100%; /* makes it responsive */ height: auto; /* keeps aspect ratio */ display: block; /* avoids inline spacing issues */ } img.no-responsive, video.no-responsive { max-width: none; height: auto; display: inline; }
3) Reduce Largest Contentful Paint(LCP)
-
Use more modern
AVIF
,WebP
image formats instead ofPNG
orJPG
andMP4
,WebM
video formats instead of heavyGIF
files to significantly improve load speed and decrease file sizes.You can use FFmpeg4, cwebp5 and ImageMagick6 to convert
PNG
/JPG
to more modern and web-friendlyAVIF
/WebP
formats.You can also use FFmpeg to resize
MP4
files or convert them toWebM
format.Here is some example commands I use for image conversion that Iāve found to return desirable results for me in most cases:
# for images ## mainly used cwebp -q 90 -near_lossless 95 -m 6 test.png -o test.webp magick test.png -quality 80 test.avif ## some other variations that had good results for me ffmpeg -i test.png -c:v libaom-av1 -crf 20 -b:v 0 test.avif ffmpeg -i test.png -q:v 80 test.webp ffmpeg -i test.png -c:v libaom-av1 -crf 30 -b:v 0 test.avif # for videos ## resize video files ffmpeg -i test.mp4 -vf scale=256:256 test_resized.mp4 ffmpeg -i test.mp4 -vf scale=308:462 -c:v libx264 -crf 32 -preset veryslow -an test_resized.mp4 ## convert to webm ffmpeg -i test.mp4 -c:v libaom-av1 -crf 40 -b:v 0 -an test.webm ffmpeg -i test.mp4 -c:v libaom-av1 -crf 30 -b:v 0 -pass 1 -an -f null -y /dev/null ffmpeg -i test.mp4 -c:v libaom-av1 -crf 30 -b:v 0 -pass 2 -an test.webm
In the case of
AVIF
andWebP
use, 96-97% of viewers are supported, for those with older legacy browsers or devices, fallback to originalPNG
orJPG
formats.Iāve written a Ruby plugin7 for my website to do just that(itās open-sourced, you can see my whole website in GitHub8) and added more arguments for more control.
- Use
fetchpriority="high"
for your largest contentful paint (LCP) image or video that is immediately visible above the fold.
-
Use
loading
attribute forimg
tag:loading="lazy"
: Great for images below the fold, but never on your LCP image (causes delays).Rule of thumb:
- Above the fold:
loading="eager"
(or omit, since eager is default). - Below the fold:
loading="lazy"
.
- Above the fold:
-
Use
decoding="async"
for secondary images:Usually improves main-thread responsiveness.
For the LCP image, async decoding can slightly delay the first paint.
Minimal risk overall, safe for secondary images.
-
Preload LCP image via
<link rel="preload">
.Jekyll Example: This is how I use
Preload
for my hero poster images(hero images that are loaded before video content is fully downloaded) in thehead.html
file of my Jekyll website:{% if page.hero-poster %}<link rel="preload" as="image" href="{{ page.hero-poster }}" fetchpriority="high">{% endif %}
and use
hero-poster
variable in the frontmatter and body of the pages that I want to use hero poster image on:--- layout: page title: Blog image: /blog/assets/robot1.png description: "Here I talk about anything, mostly technical topics." hero-poster: /blog/assets/robot1.avif --- <video autoplay muted loop playsinline width="308" height="462" poster="{{ page.hero-poster }}"> <source src="/blog/assets/robot1.webm" type="video/webm"> <source src="/blog/assets/robot1.mp4" type="video/mp4"> </video>
Rule of thumb for safe optimization
LCP/hero image: preload + fetchpriority=āhighā + eager (no async).
Other above-the-fold: optional async decoding, no lazy.
Below-the-fold: lazy + async decoding.
Donāt mark too many resources as high priority.
Keep image sizes reasonable.
- Use explicit
width
&height
for images (already did that in no.2 tip).
4) Optimize CSS
-
Compress the CSS file and disable source maps.
Example: in a Jekyll website, in
_config.yaml
:sass: style: compressed sourcemap: never
style: compressed
: minifies your CSS (removes whitespace, comments, etc.).sourcemap: never
: stops Jekyll from generatingmain.css.map
and removes the reference.Result:
-
Youāll only get a small, minified
main.css
. -
No
.map
file. -
Leanest possible setup for GitHub Pages.
-
The result is a tiny, production-ready
main.css
that PageSpeed Insights sees as fully optimized.
-
-
Use PurgeCSS9 post-build to remove css codes that are not used.
If you build your website locally, create a workflow to run
PurgeCSS
after your website build to remove unused parts of your CSS file.
-
Inline critical CSS:
Use a tool like Critical10 CSS(old, not maintained anymore), Penthouse11, Critters12 or Crittr13(recommended for Jekyll websites) to extract critical parts of your CSS that are essential for above the fold content of each of your important HTML pages and inline those parts of CSS for faster page loads.
5) Some other tips
-
Load resources locally:
If you use some website profile badges on your website (like the TryHackMe and HackTheBox badges on my website footer), itās better to download the image and serve it from your own website resources instead of direct requests for them. So that you can optimize these pictures and use smaller and more modern image formats and also prevent additional requests and cookies of third party services you send requests to.
Another benefit is you donāt need to add additional CSP rules for external resources of your website.
Example: Iāve added an option in my
_config.yml
file that gives me the option to choose to serve those pictures locally or from upstream servers:badges: hackthebox: "768488" selfhost_hackthebox: true tryhackme: "nima" selfhost_tryhackme: true
also in
footer.html
:{% if site.badges.hackthebox %} <li class="social-media-list"> {% if site.badges.selfhost_hackthebox %} <!-- local HTB badge --> <a title="Hack The Box Profile" href="https://app.hackthebox.com/profile/{{ site.badges.hackthebox }}"> {% smart_image "/assets/{{ site.badges.hackthebox }}.png" 220 50 "Hack The Box Profile" lazy "" avif async "no-responsive" %} </a> {% else %} <!-- upstream HTB badge --> <a title="Hack The Box Profile" href="https://app.hackthebox.com/profile/{{ site.badges.hackthebox }}"> <img src="https://www.hackthebox.eu/badge/image/{{ site.badges.hackthebox }}" width="220" height="50" loading="lazy" decoding="async" alt="Hack The Box" class="no-responsive"> </a> {% endif %} </li> {% endif %} {% if site.badges.tryhackme %} <li class="social-media-list"> {% if site.badges.selfhost_tryhackme %} <!-- local TryHackMe badge --> <a title="TryHackMe Profile" href="https://tryhackme.com/p/{{ site.badges.tryhackme }}"> {% smart_image "/assets/{{ site.badges.tryhackme }}.png" 329 88 "TryHackMe Profile" lazy "" avif async "no-responsive" %} </a> {% else %} <!-- upstream TryHackMe badge --> <a title="TryHackMe Profile" href="https://tryhackme.com/p/{{ site.badges.tryhackme }}"> <img src="https://tryhackme-badges.s3.amazonaws.com/{{ site.badges.tryhackme }}.png" width="329" height="88" loading="lazy" decoding="async" alt="TryHackMe Profile" class="no-responsive"> </a> {% endif %} </li> {% endif %}
-
Create a clean workflow for website build in
main
branch and publish ingh-pages
branch:Example: Here is mine so far, I build my Jekyll website locally. Iāll add critical CSS extraction later to my current build.
For
main
branch and site build:npm run build
hereās the code for
package.json
:{ "name": "nima.ninja", "version": "1.0.0", "type": "module", "scripts": { "build:jekyll": "bundle exec jekyll build", "copy-css": "node copy-css.js", "build:postcss": "postcss \"_site/css/main.css\" -o \"_site/css/main.css\"", "build": "npm run build:jekyll && npm run copy-css && npm run build:postcss" }, "devDependencies": { "postcss": "8.5.6", "postcss-cli": "11.0.1", "@fullhuman/postcss-purgecss": "7.0.2" } }
copy-css.js
copies the original css file so that I can compare it to the one PostCSS creates to make sure nothing necessary is removed.Codes for all of these files exist in my GitHub repo for my website.
For
gh-pages
branch to publish website for GitHub Pages:1ļøā£ Prepare .gitignore in gh-pages git checkout gh-pages Make sure .gitignore exists and includes only what you want ignored: _site/ CNAME css/main.original.css # Node modules node_modules/ # Jekyll build cache .jekyll-cache/ Commit if needed: git add .gitignore git commit -m "Fix .gitignore for gh-pages" 2ļøā£ Clean the branch safely Remove all tracked files except .gitignore and CNAME: git ls-files | Where-Object { $_ -notin @('.gitignore', 'CNAME') } | ForEach-Object { git rm -r --cached $_ } Optional: delete temporary or cache files: if (Test-Path ".jekyll-cache") { Remove-Item -Recurse -Force .jekyll-cache } if (Test-Path "_site/css/main.original.css") { Remove-Item -Force "_site/css/main.original.css" } Untracked files like node_modules are automatically ignored because of .gitignore. 3ļøā£ Copy _site contents into branch root robocopy "_site" "." /E 4ļøā£ Stage and commit only the intended files git status (optional: to see if there's anything) git add . git commit -m "Update site from _site" git push origin gh-pages
The result is built website code committed and pushed to the
gh-pages
branch of my website14 on GitHub.
References
-
Icon made by zero_wing from www.flaticon.comĀ ↩
-
My Ruby plugin for AVIF/WebP image format selection and fallback to PNG/JPGĀ ↩
-
Critical: Extract & Inline Critical-path CSS in HTML pagesĀ ↩
-
Critters: A Webpack plugin to inline your critical CSS and lazy-load the rest.Ā ↩
-
Crittr: High performance critical css extraction with a great configuration abilitiesĀ ↩