A place to gather resources, issues, and solutions to optimize website speed

Regular Contributor | Partner

Optimizing a Hubspot site to load quickly is difficult at the moment. Good solutions are scattered and the current documentation has gaps. Let's use this post to donate solutions that work and start a conversation to fix what doesn't. 

 

This guides needs two sections:

  1. Working solutions that everybody can do
  2. Unique Hubspot issues

 

1. Working Solutions

If you're new, start with the Hubspot CMS-Boilerplate—it has all the answers. Carefully step through the docs until you're comfortable with local development (don't forget Github Actions). 

 

Snippets Coming Not That Soon

  • Webpack config
  • Hubspot Autouploader
  • SASS/SCSS
  • Image optimization
  • Image deferral 
  • Critical path CSS
  • Videos
  • Font optimization

Researching and Need Help

  • Deferring Hubspot analytics 
  • Separate CSS/JS bundles at page level

 

2. Unique Hubspot Issues 

Some features (like tracking codes) are always loaded—Hubspot without tracking analytics isn't Hubspot. Other features (like contact forms) are optional but require creative implementations to prevent nerfed PageSpeed scores. 

 

The Worst (Only?) Offenders 

 

1. Hubspot Forms

Embedding a Hubspot form can drop your pagespeed score by up to 20% on mobile. Why do the form and "//js.hsforms.net/forms/v2.js" script have such a huge cost? 

 

If you are 100% sure that your form will appear "below the fold", this is a working solution: 

<!-- Add somewhere in your HTML -->
<div id="form-holder"></div>

 

/********************
   Add somewhere in your JS
  ******************/
  
  function userScroll() {
    var hsform = document.createElement('script');
    hsform.src='//js.hsforms.net/forms/v2.js';
    hsform.async = true;
    document.head.appendChild(hsform);
    const currentScroll = window.pageYOffset;
    if (currentScroll > 0) {
      if (window.hbspt) {
        window.hbspt.forms.create({
          portalId: "[your-portal-id]",
          formId: "[your-form-id]",
          target: '#form-holder'
        });
        window.removeEventListener('scroll', userScroll, false);
      }
    }
  }

  document.addEventListener('readystatechange', event => {
    if (event.target.readyState === 'complete') {
      window.addEventListener('scroll', userScroll, false);
    }
  });

Please reply if you have a better solution or have an idea to make this work for forms that appear "above the fold".

 

Jon McLaren pointed out some issues with this solution in the developer Slack channel. 

  • Scroll is not the best trigger for performance reasons
  • Not a good idea to depend on scrolling for important content
  • This could trigger when a lot of other things are also happening 

 

Other posts talking about this: 

 

2. Hubspot Chat

It's bad but I don't have numbers ready yet. 

 

3. Mystery Files

It's not clear if these files are needed but they are automatically included. As far as I know, the following files are always loaded and cannot be removed: 

- ...scriptloader/8020292.js (802029s.hs-sites.com)

- ...159.../8020292.js (js.hs-analytics.com)

- /8020292.js (js.hs-banner.com)

- /bundles/project.js (static.hsappstatic.net)

- /collectedforms.js  (js.hscollectedforms.net)

- sometimes common.js (no longer forced)

- sometimes layout.js (no longer forced)

 

4. Cache Policy

Should we be able to define custom cache policies to help repeat visits load faster? Is there a downside to setting these to far in the future and using a hash to load new versions?

 

5. Which files are required for a locally developed Hubspot site to publish? Can they be combined to make fewer HTTP requests?

 

6. Does it ever make sense to build a Hubspot site that doesn't include standard Hubspot analytics? 

 

7. Which of these mystery files am I actually generating myself by accident? Can I prevent them from loading?  

 

8. Are there any other hidden files or bloated files that we don't need 100% of the time? 

 

9. Maybe Google PageSpeed scores aren't a good reflection of most user experiences on your site? 

 

---------------------------------------------

Reading List

Hubspot Guides 

Blog Posts

Hubspot Courses

Good To Know

 

Last Updated: 7/16/20 (Published but in progress...)

12 Replies 12
Occasional Contributor | Diamond Partner

CTA's have been bogging things down. a few customers with 3-4 cta's were making many unnecessary calls

Community Manager

Thank you for sharing this @theAndreyK! I think is really useful! 


¿Sabías que la Comunidad está disponible en Español?
¡Participa hoy en conversaciones en el idioma de tu preferencia,cambiando el idioma en tus configuraciones!

Did you know that the Community is available in other languages?
Join regional conversations by changing your language settings !


Reply
0 Upvotes
Regular Contributor | Platinum Partner
export default () => {
  let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"))
  if (lazyImages.length < 1) return

  // If the browser does NOT support IntersectionObserver
  if ( !('IntersectionObserver' in window) ) {
    lazyImages.forEach(lazyImage => updateSrc(lazyImage) )
    return
  }

  let lazyImageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        let lazyImage = entry.target
        updateSrc(lazyImage)
        lazyImageObserver.unobserve(lazyImage)
      }
    })
  }, {rootMargin: '0px 0px 200px 0px'})
  // load 200px in advance

  lazyImages.forEach(lazyImage => {
    lazyImageObserver.observe(lazyImage)
  })
}

function updateSrc(lazyImage) {
  if (lazyImage.dataset.src) {
    lazyImage.src=lazyImage.dataset.src
  }
  if (lazyImage.dataset.srcset) {
    lazyImage.srcset = lazyImage.dataset.srcset
  }
  if (lazyImage.dataset.sizes) {
    lazyImage.sizes = lazyImage.dataset.sizes
  }

  lazyImage.classList.add('loaded')
}

Simple snippet for adding lazyload to your images, can be translated for pictures and video really simple

Occasional Contributor

Nice! I'll include a warning here -  I always recommend against prematurely optimising images using the lazy loading technique. Doing it this way can have some negative side effects:

 

1. If for any reason JS doesnt run - you're going to be missing a bunch of images.

2. It's harder for search engines to crawl your images (if you care about this)

3. If not done correctly, it can make performance worse. You need to make sure your images have a height/width set otherwise you're going to trigger a content reflow.

 

There are some cases in which it's good, but just adding without checking your analytics and understanding your audience better is not a good idea.

Regular Contributor | Platinum Partner

I have to agree! The script is triggered when you set a lazy class and pass the data-src attribute, so you have full control on what images you want to optimize, but use this wisely.
We always include a no-js fallback to prevent any issues.

Reply
0 Upvotes
HubSpot Employee

For lazy loading I recommend using native browser lazy loading. Does not require javascript and trasfers the optimization to the browser to handle. Javascript lazy loading as a fallback if native is not supported is also decent method.
https://web.dev/native-lazy-loading/
@ChadP  shared his native lazy loading script which is a fallback if native lazy loading is not available.

Jon McLaren

Sr. CMS Developer Advocate

If my reply answered your question, please mark it as a solution, to make it easier for others to find.

Top Contributor

Nice one @theAndreyK !

Love the idea.

 

So gonna share how I started to implement fragmented CSS for a theme (based on boilerplate).

  1. Go to templates/layouts/base.html
  2. Look for the inject of the main.css file (at the time, line 8):
    {{ require_css(get_asset_url('../../css/main.css')) }}
    and replace it with
    {% if load_css %}
        {{ require_css(get_asset_url(load_css)) }}
    {% else %}
        {{ require_css(get_asset_url('../../css/main.css')) }}

    {% endif %}
  3. Go to the templates folder.
  4. In all your templates you want to define the load_css variable before the call of the base.html, so for example for the blog template it will be something like this:
    {% set load_css = "../../css/blog.css" %}
    {% extends "./layouts/base.html" %}

    Repeat for each template file.

  5. Create the {template-name}.css files that will call just the needed CSS files based on the template/page. I would also suggest to have a "general" one that will contain the across website element that you will also need such _reset, _normalize.css _typograghy.css etc. So it's easier to keep it right and simpler.
    Example of my blog.css file:
    {% include './general.css' %}
    {% include './_blog.css' %}
    My version is based on an outdated boilerplate version so double check your needs here*. Ideally you will get rid of the theme-overrides.css and integrate where applies.
    Notice the load_css is calling the new blog.css file (not _blog.css) because it not a fragmented one (it contains the calls to those fragmented files that would be used).
    Repeat for all you want to split, for example blog.css, lp.css, page.css and system.css.

 

Explanation:

The variable load_css will load a css file, for example blog.css. It will contains a similar structure to the current main.css file, but instead calling all the fragmented css files will call just based on the template you are visting and it needs.
The only "issue" is that on the website side, you can't really fragment that much because you don't know if it will include a module, a form a table etc. But it definetly worth it for the blog side.

More to come!

@jmclaren 

Regarding the new dnd I am very concern about how HS manages background images for the sections (as background-image CSS properties). It would be much more worth it to have them as image elements so you can lazyload them too (that is what we used to do, and I could do a CM for it, but I would like to use as much HS tools this time instead "hacking" ways that will increase dificulty for the marketers). Other option is make them CSS class dependent so will only load after you scroll until that point, but not as good as the previous suggestion -unnecesary JS handle-.

HubSpot Employee

Hey @Gonzalo responding to your feedback about dnd_areas and background images.

I will relay this feedback internally. To be honest this lazy loading technique you're describing is effectively a "hack" to get browsers to do what you want. For HubSpot to provide that functionality it would require HubSpot injecting a JavaScript file to load that the developer has no control over.

 

It's also not at all unlikely that a form of native lazy loading will come for background images after developers have seen the benefits of native lazy loading. Native lazy loading is preferrable for a lot of reasons - it is better for accessibility, you're less likely to affect Cumulative Layout Shift because you are not swapping say a 1px by 1px image with a real full-res image, it also transfers the work of optimizing the performance to the web browser.

For background images - CLS and accessibility is already kind of factored out, so the main goal is just serving the image in a performant way.

 

There is by far more intelligent and meaningful optimization that a browser can do than developers can do using JavaScript. Any JavaScript needed to lazy load requires a blocking JS script in the head of a page. Native lazy loading removes the file/script all-together removing both a network request and script execution. It can also take advantage of it's awareness of what images the browser already has cached.   

 

Meaning at some point possibly soon, the JavaScript would become unnecessary, and a breaking change would be needed to upgrade websites to the better native implementation.

For img elements we're currently thinking through how we could deliver the best experience for developers and marketers to be able to control native lazy loading. Would love to hear any thoughts anyone has regarding that.

 

Jon McLaren

Sr. CMS Developer Advocate

If my reply answered your question, please mark it as a solution, to make it easier for others to find.

Regular Contributor

Hi Jon! 

 

I think the option to use native lazy loading would be great! I recently created a custom image module that incorperates lazy loading, but having this loading="lazy", native browser option in hubspot would a +1. 

 

Here's a preview into the code: 

 

 

{% if module.image_field.src %}
	{% set sizeAttrs = 'width="{{ module.image_field.width }}" height="{{ module.image_field.height }}"' %}
	{% if module.image_field.size_type == 'auto' %}
		{% set sizeAttrs = 'style="max-width: 100%; height: auto;"' %}
	{% elif module.image_field.size_type == 'auto_custom_max' %}
		{% set sizeAttrs = 'width="100%" height="auto" style="max-width: {{ module.image_field.max_width }}px; max-height: {{ module.image_field.max_height }}px"' %}
	{% endif %}
	<img src="{{ module.image_field.src }}" alt="{{ module.image_field.alt }}" {{ sizeAttrs }} loading="lazy">
{% endif %}

 

 

The only thing is, this is a LOT of work to switch out our old image modules with the new lazy loading one. If there was an option to flip a switch to automatically turn all the OLD image modules to native lazy loading that would be AMAZING. Because we still haven't switched all our image modules, as you can see it's very time consuming and feel as if I will never get finished with it as there are some pages like blogs that don't have an image modules used in the design manager (and this is just one example) etc etc.. very convuluted! 

 

Reply
0 Upvotes
Top Contributor

Hey @jmclaren 

 

I didn't explained well as I wanted to provide the "current solution" I am doing, so it was a confusing feedback.

I ment to say that ideally, you will handle the current background-image in the new DND as <img loading="lazy" ... >. This will require some extra tweaks, but as today its pretty easy to do something like:

.dnd-section{
   position: relative
}
.dnd-section > .background{
   position: absolute;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
   z-index: -1;
   object-fit: cover; /** or add extra controls here */
}

This can be included as default and will be easily ovewritable if needed.

 

Thank you 😉

Regular Contributor

Some awesome resources here, I am now following! 

 

Don't forget loading jQuery from the footer, can help a lot in certain situations as well. 

HubSpot Employee

Related to loading jQuery from the footer - we've released a guide which shows you how to upgrade to the latest version of jQuery. That may give you some performance benefits, along with new features.


Also an update on lazy loading images.

Jon McLaren

Sr. CMS Developer Advocate

If my reply answered your question, please mark it as a solution, to make it easier for others to find.