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.
Table of contents
- 1) Use Cloudflare
- 2) Reduce Cumulative Layout Shift(CLS)
- 3) Reduce Largest Contentful Paint(LCP)
- 4) Optimize CSS
- 5) Minify JS scripts
- 6) Some other tips
- References
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).
-
Less noticed but still very important: 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 enable only 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
Use it 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 limit 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.

There are many more Cloudflare features that can help us run a more fast, optimized and secure website. I may write a post in the future dedicated to the most useful Cloudflare settings for websites.
2) Reduce Cumulative Layout Shift(CLS)
-
Use hardcoded width and height for
imgandvideotags -
Use a CSS class to still support responsive after previous tip
This is the way I handle it in my Jekyll website, in
base.scssfile:// 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 media formats
Use
AVIF,WebPimage formats instead ofPNGorJPGandMP4,WebMvideo formats instead of heavyGIFfiles to significantly improve load speed and decrease file sizes.You can use FFmpeg4, cwebp5 and ImageMagick6 to convert
PNG/JPGto more modern and web-friendlyAVIF/WebPformats.You can also use FFmpeg to resize
MP4files or convert them toWebMformat.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 ### convert png/jpg to webp/avif: cwebp -q 90 -near_lossless 95 -m 6 test.png -o test.webp magick test.png -quality 80 test.avif ### resize png/jpg, keep aspect ratio(example: use 130x for width: 130, use x130 for height: 130 ): magick why-we-sleep-book.jpg -resize 130x why-we-sleep-book-thumb.jpg ## 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.webmIn the case of
AVIFandWebPuse, 96-97% of viewers are supported, for those with older legacy browsers or devices, fallback to originalPNGorJPGformats.I’ve written a Ruby plugin7 for my website to do just that and added more arguments for more control. It’s open-sourced, you can see my whole website in GitHub8.
-
Use
fetchpriority="high"Use it for your largest contentful paint (LCP) image or video that is immediately visible above the fold.
-
Use
loadingattribute forimgtagloading="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 imagesUsually 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
Preloadfor my hero poster images(hero images that are loaded before video content is fully downloaded) in thehead.htmlfile of my Jekyll website:{% if page.hero-poster %}<link rel="preload" as="image" href="{{ page.hero-poster }}" fetchpriority="high">{% endif %}and use
hero-postervariable 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&heightfor imageswe already did that for reducing Cumulative Layout Shift in tip number 2.
4) Optimize CSS
-
Compress the CSS file and disable source maps
Example: in a Jekyll website, in
_config.yaml:sass: style: compressed sourcemap: neverstyle: compressed: minifies your CSS (removes whitespace, comments, etc.).sourcemap: never: stops Jekyll from generatingmain.css.mapand removes the reference.Result:
-
You’ll only get a small, minified
main.css. -
No
.mapfile. -
Leanest possible setup for GitHub Pages.
-
The result is a tiny, production-ready
main.cssthat PageSpeed Insights sees as fully optimized.
-
-
Use PurgeCSS post-build to remove CSS codes that are not used
If you build your website locally, create a workflow to run
PurgeCSS9 after your website build to remove unused parts of your CSS file.
-
Inline critical CSS
Use a tool like Critical10 CSS, Penthouse11, Critters12 or Crittr13 to extract critical parts of CSS that are used for above the fold content of each of your important HTML pages and inline those parts of CSS for faster page loads. Use nonce for inline styles.
5) Minify JS scripts
Minify inline and external JS scripts with a tool like Terser14. Also use nonce for inline scripts.
6) 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.ymlfile 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: truealso 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 clean workflows for website build and publish
You can have two branches, one for website build and another for publish.
Example: I use
mainbranch for build andgh-pagesfor publish.Here is my local build pipeline for
mainbranch , fully automated:npm run buildhere’s the code for
package.json:{ "name": "nima.ninja", "version": "1.0.0", "type": "module", "scripts": { "update:badges": "node build/update-badges.js", "build:jekyll:1": "bundle exec jekyll build", "postcss": "postcss \"_site/css/main.css\" -o \"_site/css/main.css\"", "copy:minified-css": "node build/copy-file.js \"_site/css/main.css\" \"src/css/temp.main.css\"", "postcss:critical": "postcss \"_site/css/main.css\" -o \"_site/css/main-critical.css\" --env critical", "critical:home:mobile": "critical _site/index.html --base=_site --css=_site/css/main-critical.css --width=412 --height=667 > src/_includes/critical-home-mobile.css || echo 'Skipped home mobile critical'", "critical:home:tablet": "critical _site/index.html --base=_site --css=_site/css/main-critical.css --width=768 --height=800 > src/_includes/critical-home-tablet.css || echo 'Skipped home tablet critical'", "critical:home:desktop": "critical _site/index.html --base=_site --css=_site/css/main-critical.css --width=1440 --height=800 > src/_includes/critical-home-desktop.css || echo 'Skipped home desktop critical'", "critical:post:mobile": "critical _site/blog/2025/quick-tips-for-a-better-website.html --base=_site --css=_site/css/main-critical.css --width=412 --height=667 > src/_includes/critical-post-mobile.css || echo 'Skipped post mobile critical'", "critical:post:tablet": "critical _site/blog/2025/quick-tips-for-a-better-website.html --base=_site --css=_site/css/main-critical.css --width=768 --height=800 > src/_includes/critical-post-tablet.css || echo 'Skipped post tablet critical'", "critical:post:desktop": "critical _site/blog/2025/quick-tips-for-a-better-website.html --base=_site --css=_site/css/main-critical.css --width=1440 --height=800 > src/_includes/critical-post-desktop.css || echo 'Skipped post desktop critical'", "critical:page:mobile": "critical _site/books/index.html --base=_site --css=_site/css/main-critical.css --width=412 --height=667 > src/_includes/critical-page-mobile.css || echo 'Skipped page mobile critical'", "critical:page:tablet": "critical _site/books/index.html --base=_site --css=_site/css/main-critical.css --width=768 --height=800 > src/_includes/critical-page-tablet.css || echo 'Skipped page tablet critical'", "critical:page:desktop": "critical _site/books/index.html --base=_site --css=_site/css/main-critical.css --width=1440 --height=800 > src/_includes/critical-page-desktop.css || echo 'Skipped page desktop critical'", "critical:home": "npm run critical:home:mobile && npm run critical:home:tablet && npm run critical:home:desktop", "critical:post": "npm run critical:post:mobile && npm run critical:post:tablet && npm run critical:post:desktop", "critical:page": "npm run critical:page:mobile && npm run critical:page:tablet && npm run critical:page:desktop", "critical": "npm run critical:home && npm run critical:post && npm run critical:page", "minify:inlinejs": "terser \"src/_includes/lazy-load-back-to-top.js\" -o \"src/_includes/lazy-load-back-to-top.min.js\" -c -m", "build:jekyll:2": "bundle exec jekyll build", "minify:js": "terser \"_site/js/back-to-top.js\" -o \"_site/js/back-to-top.js\" -c -m && terser \"_site/js/particles.js\" -o \"_site/js/particles.js\" -c -m && terser \"_site/js/smooth-fragments.js\" -o \"_site/js/smooth-fragments.js\" -c -m", "copy:back": "node build/copy-file.js \"src/css/temp.main.css\" \"_site/css/main.css\"", "copy:root-files": "node build/copy-file.js src/README.md README.md && node build/copy-file.js src/license.md license.md", "build": "npm run update:badges && npm run build:jekyll:1 && npm run postcss && npm run copy:minified-css && npm run postcss:critical && npm run critical && npm run minify:inlinejs && npm run build:jekyll:2 && npm run minify:js && npm run copy:back && npm run copy:root-files" }, "scriptsComments": { "update:badges": "Generate README.md from README.template.md by replacing placeholders ({{LAST_UPDATED}}, {{RUBY_VERSION}}, {{JEKYLL_VERSION}}, {{NODE_VERSION}}, {{NPM_VERSION}}) with real values. Do not edit README.md directly — edit README.template.md instead.", "build:jekyll:1": "First Jekyll build: generate HTML pages.", "postcss": "Run PostCSS with PurgeCSS to remove unused CSS for live site.", "copy:minified-css": "Save a temp copy of main.css so it can be restored later.", "postcss:critical": "Run PostCSS again to generate main-critical.css, removing selectors that should not be inlined by Critical (like #toTopButton).", "critical:*": "Generate critical CSS for different breakpoints and pages, reading main-critical.css.", "minify:inlinejs": "Minify inline JS scripts", "build:jekyll:2": "Second Jekyll build: inject generated Critical CSS includes and generated inline JS scripts includes into HTML pages.", "minify:js": "Minify JS files.", "copy:back": "Restore the full PurgeCSS main.css to _site for live site.", "copy:root-files": "Copy README.md and license.md from src/ to repository root so they exist at the repo root for GitHub display and are not only in _site", "build": "Full build pipeline: update badges, build site, process CSS, generate Critical CSS, rebuild HTML with critical CSS and minified inline scripts, minify JS files, restore main CSS." }, "devDependencies": { "@fullhuman/postcss-purgecss": "7.0.2", "critical": "7.2.1", "postcss": "8.5.6", "postcss-cli": "11.0.1", "terser": "5.44.0" } }The script comments are clear about each of those build lines.
Codes for all of these files exist in my website GitHub repo.
For
gh-pagesbranch which is used for publishing website files for GitHub Pages:# Switch to gh-pages branch git checkout gh-pages # Ensure .gitignore contains only: # _site/, CNAME, node_modules/, .jekyll-cache/ git add .gitignore git commit -m "Fix .gitignore for gh-pages" # Clean branch root while keeping .git, .gitignore, node_modules and _site Get-ChildItem -Force | Where-Object { $_.Name -notin @('.git', '.gitignore', 'node_modules', '_site') } | Remove-Item -Recurse -Force # Optional: Remove temporary Jekyll cache if exists if (Test-Path ".jekyll-cache") { Remove-Item -Recurse -Force .jekyll-cache } # Copy freshly built _site contents into repo root robocopy "_site" "." /E # Stage all changes git add . # Optional: Show staged changes git status # Commit & push automatically if there are changes if (-not (git diff --cached --quiet)) { git commit -m "Update site from _site" git push origin gh-pages } else { Write-Output "No changes to commit. Nothing to push." }The result: built website code is committed and pushed to the
gh-pagesbranch of my website15 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 ↩
-
Terser: JavaScript parser, mangler and compressor toolkit for ES6+ ↩