Landing page optimization doesn’t happen overnight. That’s why marketers get frustrated — and often give up. If you want better landing pages, focus on collecting data. You should design your landing pages based on what you already know about your audience, but you’ll collect even more information as more people visit the page. Converting that data into informed decisions about your marketing funnel can produce more leads and sales. Today, I’m going to teach you my best landing page optimization tips and tricks so you can attract more prospects and convert more customers. If you’d like to skip around, here’s…
Conditioner And Progressive Enhancement Sitting In A Tree
Before we proceed, I need to get one thing across:
Conditioner is not a framework for building web apps.
Instead, it’s aimed at websites. The distinction between websites and web apps is useful for the continuation of this story. Let me explain how I view the overall difference between the two.
Examples of content-oriented websites are for instance: Wikipedia, Smashing Magazine, your local municipality website, newspapers, and webshops. Web apps are often found in the utility area, think of web-based email clients and online maps. While also presenting content, the focus of web apps is often more on interacting with content than presenting content. There’s a huge grey area between the two, but this contrast will help us decide when Conditioner might be effective and when we should steer clear.
As stated earlier, Conditioner is all about websites, and it’s specifically built to deal with that third act:
The Troublesome Third Act
A class is added to an HTML element.
The querySelectorAll method is used to get all elements assigned the class.
A for-loop traverses the NodeList returned in step 2.
Let’s quickly put this workflow in code by adding autocomplete functionality to an input field. We’ll create a file called autocomplete.js and add it to the page using a <script> tag.
// our autocomplete logic
<input type="text" class="autocomplete"/>
var inputs = document.querySelectorAll('.autocomplete');
for (var i = 0; i < inputs.length; i++)
Suppose we’re now told to add another functionality to the page, say a date picker, it’s initialization will most likely follow the same pattern. Now we’ve got two for-loops. Add another functionality, and you’ve got three, and so on and so on. Not the best.
While this works and keeps you off the street, it creates a host of problems. We’ll have to add a loop to our initialization script for each functionality we add. For each loop we add, the initialization script gets linked ever tighter to the document structure of our website. Often the initialization script will be loaded on each page. Meaning all the querySelectorAll calls for all the different functionalities will be run on each and every page whether functionality is defined on the page or not.
For me, this setup never felt quite right. It always started out “okay,” but then it would slowly grow to a long list of repetitive for-loops. Depending on the project it might contain some conditional logic here and there to determine if something loads on a certain viewport or not.
if (window.innerWidth <= 480)
// small viewport for-loops here
Eventually, my initialization script would always grow out of control and turn into a giant pile of spaghetti code that I would not wish on anyone.
Something needed to be done.
That stack of spaghetti loops though, I wanted to get rid them so badly.
We’ll quickly update our script to use data attributes instead of classes.
<input type="text" data-module="autocomplete">
var inputs = document.querySelectorAll('[data-module=autocomplete]');
for (var i = 0; i < inputs.length; i++)
But hang on, this is nearly the same setup; we’ve only replaced .autocomplete with [data-module=autocomplete]. How’s that any better? It’s not, you’re right. If we add an additional functionality to the page, we still have to duplicate our for-loop — blast! Don’t be sad though as this is the stepping stone to our killer for-loop.
Watch what happens when we make a couple of adjustments.
<input type="text" data-module="createAutocomplete">
var elements = document.querySelectorAll('[data-module]');
for (var i = 0; i < elements.length; i++)
var name = elements[i].getAttribute('data-module');
var factory = window[name];
Now we can load any functionality with a single for-loop.
Find all elements on the page with a data-module attribute;
Loop over the node list;
Get the name of the module from the data-module attribute;
This basic setup has some other advantages as well:
The init script no longer needs to know what it loads; it just needs to be very good at this one little trick.
The init script does not search for modules that are not there, i.e. no wasted DOM searches.
The init script is done. No more adjustments are needed. When we add functionality to the page, it will automatically be found and will simply work.
So What About This Thing Called Conditioner?
We finally have our single loop, our one loop to rule all other loops, our king of loops, our hyper-loop. Ehm. Okay. We’ll just have to conclude that our is a loop of high quality and is so flexible that it can be re-used in each project (there’s not really anything project specific about it). That does not immediately make it library-worthy, it’s still quite a basic loop. However, we’ll find that our loop will require some additional trickery to really cover all our use-cases.
With the one loop, we are now loading our functionality automatically.
We assign a data-module attribute to an element.
We add a <script> tag to the page referencing our functionality.
The loop matches the right functionality to each element.
Let’s take a look at what we need to add to our loop to make it a bit more flexible and re-usable. Because as it is now, while amazing, we’re going to run into trouble.
It would be handy if we moved the global functions to isolated modules. This prevents pollution of the global scope. Makes our modules more portable to other projects. And we’ll no longer have to add our <script> tags manually. Fewer things to add to the page, fewer things to maintain.
When using our portable modules across multiple projects (and/or pages) we’ll probably encounter a situation where we need to pass configuration options to a module. Think API keys, labels, animation speeds. That’s a bit difficult at the moment as we can’t access the for-loop.
With the ever-growing diversity of devices out there we will eventually encounter a situation where we only want to load a module in a certain context. For instance, a menu that needs to be collapsed on small viewports. We don’t want to add if-statements to our loop. It’s beautiful as it is, we will not add if statements to our for-loop. Never.
That’s where Conditioner can help out. It encompasses all above functionality. On top of that, it exposes a plugin API so we can configure and expand Conditioner to exactly fit our project setup.
Let’s make that 1 Kilobyte jump and replace our initialization loop with Conditioner.
Switching To Conditioner
We can get the Conditioner library from the GitHub repository, npm or from unpkg. For the rest of the article, we’ll assume the Conditioner script file has been added to the page.
Conditioner will now automatically lazy load ./autocomplete.js, and once received, it will call the module.default function and pass the element as a parameter.
Defining our autocomplete as ./autocomplete.js is very verbose. It’s difficult to read, and when adding multiple modules on the page, it quickly becomes tedious to write and error prone.
This can be remedied by overriding the moduleSetName action. Conditioner views the data-module value as an alias and will only use the value returned by moduleSetName as the actual module name. Let’s automatically add the js extension and relative path prefix to make our lives a bit easier.
<input type="text" data-module="autocomplete"/>
// converts module aliases to paths
moduleSetName: (name) => `./$ name .js`
Now we can set data-module to autocomplete instead of ./autocomplete.js, much better.
That’s it! We’re done! We’ve setup Conditioner to load ES Modules. Adding modules to a page is now as easy as creating a module file and adding a data-module attribute.
The plugin architecture makes Conditioner super flexible. Because of this flexibility, it can be modified for use with a wide range of module loaders and bundlers. There’s bootstrap projects available for Webpack, Browserify and RequireJS.
Please note that Conditioner does not handle module bundling. You’ll have to configure your bundler to find the right balance between serving a bundled file containing all modules or a separate file for each module. I usually cherry pick tiny modules and core UI modules (like navigation) and serve them in a bundled file while conditionally loading all scripts further down the page.
Alright, module loading — check! It’s now time to figure out how to pass configuration options to our modules. We can’t access our loop; also we don’t really want to, so we need to figure out how to pass parameters to the constructor functions of our modules.
Passing Configuration Options To Our Modules
I might have bent the truth a little bit. Conditioner has no out-of-the-box solution for passing options to modules. There I said it. To keep Conditioner as tiny as possible I decided to strip it and make it available through the plugin API. We’ll explore some other options of passing variables to modules and then use the plugin API to set up an automatic solution.
The easiest and at the same time most banal way to create options that our modules can access is to define options on the global window scope.
We’ve only eliminated the dataset call, i.e. seven characters. Not the biggest improvement, but we’ve opened the door to take this a bit further.
Suppose we have multiple autocomplete modules on the page, and each and every single one of them requires the same API key. It would be handy if that API key was supplied automagically instead of having to add it as a data attribute on each element.
We can improve our developer lives by adding a page level configuration object.
const pageOptions =
// the module alias
key: 'abc123' // api key
// the name of the module and the element it's being mounted to
moduleSetConstructorArguments: (name, element) => ([
// merge the default page options with the options set on the element it self
As our pageOptions variable has been defined with const it’ll be block-scoped, which means it won’t pollute the global scope. Nice.
Using Object.assign we merge an empty object with both the pageOptions for this module and the dataset DOMStringMap found on the element. This will result in an options object containing both the source property and the key property. Should one of the autocomplete elements on the page have a data-key attribute, it will override the pageOptions default key for that element.
That’s some top-notch developer convenience right there.
By having added this tiny plugin, we can automatically pass options to our modules. This makes our modules more flexible and therefore re-usable over multiple projects. We can still choose to opt-out and use dataset or globally scope our configuration variables (no, don’t), whatever fits best.
Our next challenge is the conditional loading of modules. It’s actually the reason why Conditioner is named Conditioner. Welcome to the inner circle!
Conditionally Loading Modules Based On User Context
Back in 2005, desktop computers were all the rage, everyone had one, and everyone browsed the web with it. Screen resolutions ranged from big to bigger. And while users could scale down their browser windows, we looked the other way and basked in the glory of our beautiful fixed-width sites.
I’ve rendered an artist impression of the 2005 viewport:
I’ve applied this knowledge to our artist impression below.
Holy smokes! That’s a lot of viewports.
Today, someone might visit your site on a small mobile device connected to a crazy fast WiFi hotspot, while another user might access your site using a desktop computer on a slow tethered connection. Yes, I switched up the connection speeds — reality is unpredictable.
And to think we were worried about users resizing their browser window. Hah!
Note that those million viewports are not set in stone. A user might load a website in portrait orientation and then rotate the device, (or, resize the browser window), all without reloading the page. Our websites should be able to handle this and load or unload functionality accordingly.
With Conditioner in place, let’s configure it as a gatekeeper and have it load modules based on the current user context. The user context contains information about the environment in which the user is interacting with your functionality. Some examples of environment variables influencing context are viewport size, time of day, location, and battery level. The user can also supply you with context hints, for instance, a preference for reduced motion. How a user behaves on your platform will also tell you something about the context she might be in, is this a recurring visit, how long is the current user session?
The better we’re able to measure these environment variables the better we can enhance our interface to be appropriate for the context the user is in.
We’ll need an attribute to describe our modules context requirements so Conditioner can determine the right moment for the module to load and to unload. We’ll call this attribute data-context. It’s pretty straightforward.
Let’s leave our lovely autocomplete module behind and shift focus to a new module. Our new section-toggle module will be used to hide the main navigation behind a toggle button on small viewports.
Since it should be possible for our section-toggle to be unloaded, the default function returns another function. Conditioner will call this function when it unloads the module.
We don’t need the toggle behavior on big viewports as those have plenty of space for our menu (it’s a tiny menu). We only want to collapse our menu on viewports more narrow than 30em (this translates to 480px).
The data-context attribute will trigger Conditioner to automatically load a context monitor observing the media query (max-width:30em). When the user context matches this media query, it will load the module; when it does not, or no longer does, it will unload the module.
Monitoring happens based on events. This means that after the page has loaded, should the user resize the viewport or rotate the device, the user context is re-evaluated and the module is loaded or unloaded based on the new observations.
You can view monitoring as feature detection. Where feature detection is about an on/off situation, the browser either supports WebGL, or it doesn’t. Context monitoring is a continuous process, the initial state is observed at page load, but monitoring continues after. While the user is navigating the page, the context is monitored, and observations can influence page state in real-time.
The media query monitor is the only monitor that is available by default. Adding your own custom monitors is possible using the plugin API. Let’s add a visible monitor which we’ll use to determine if an element is visible to the user (scrolled into view). To do this, we’ll use the brand new IntersectionObserver API.
// the monitor hook expects a configuration object
// the name of our monitor with the '@'
// the create method will return our monitor API
create: (context, element) => (
// current match state
// called by conditioner to start listening for changes
new IntersectionObserver(entries =>
// update the matches state
this.matches = entries.pop().isIntersecting == context;
// inform Conditioner of the state change
We now have a visible monitor at our disposal.
Let’s use this monitor to only load images when they are scrolled in to view.
A red cat eating a yellow bird
The lazyImage module will extract the link text, create an image element, and set the link text to the alt text of the image.
export default (element) =>
// store original link text
const text = element.textContent;
// replace element text with image
const image = new Image();
image.src = element.href;
return () =>
// restore original element state
element.innerHTML = text
When the anchor is scrolled into view, the link text is replaced with an img tag.
Because we’ve returned an unload function the image will be removed when the element scrolls out of view. This is most likely not what we desire.
We can remedy this behavior by adding the was operator. It will tell Conditioner to retain the first matched state.
A red cat eating a yellow bird
There are three other operators at our disposal.
The not operator lets us invert a monitor result. Instead of writing @visible false we can write not @visible which makes for a more natural and relaxed reading experience.
Last but not least, we can use the or and and operators to string monitors together and form complex context requirements. Using and combined with or we can do lazy image loading on small viewports and load all images at once on big viewports.
data-context="was @visible and @media (max-width:30em) or @media (min-width:30em)">
A red cat eating a yellow bird
We’ve looked at the @media monitor and have added our custom @visible monitor. There are lots of other contexts to measure and custom monitors to build:
Tap into the Geolocation API and monitor the location of the user @location (near: 51.4, 5.4) to maybe load different scripts when a user is near a certain location.
Imagine a @time monitor, which would make it possible to enhance a page dynamically based on the time of day @time (after 20:00).
By moving context monitoring outside of our modules, our modules have become even more portable. If we need to add collapsible sections to one of our pages, it’s now easy to re-use our section toggle module, because it’s not aware of the context in which it’s used. It just wants to be in charge of toggling something.
And this is what Conditioner makes possible, it extracts all distractions from the module and allows you to write a module focused on a single task.
Conditioner exposes a total of three methods. We’ve already encountered the hydrate and addPlugin methods. Let’s now have a look at the monitor method.
The monitor method lets us manually monitor a context and receive context updates.
const monitor = conditioner.monitor('@media (min-width:30em)');
monitor.onchange = (matches) =>
// called when a change to the context was observed
As a quick example, I’ve built a React <ContextRouter> component that uses Conditioner to monitor user context queries and switch between views. It’s heavily inspired by React Router so might look familiar.
<Context query="@media (min-width:30em)"
component= FancyInfoGraphic />
// fallback to use on smaller viewports
I hope someone out there is itching to convert this to Angular. As a cat and React person I just can’t get myself to do it.
Replacing our initialization script with the killer for loop created a single entity in charge of loading modules. From that change, automatically followed a set of requirements. We used Conditioner to fulfill these requirements and then wrote custom plugins to extend Conditioner where it didn’t fit our needs.
Not having access to our single for loop, steered us towards writing more re-usable and flexible modules. By switching to dynamic imports we could then lazy load these modules, and later load them conditionally by combining the lazy loading with context monitoring.
With conditional loading, we can quickly determine when to send which module over the connection, and by building advanced context monitors and queries, we can target more specific contexts for enhancement.
By combining all these tiny changes, we can speed up page load time and more closely match our functionality to each different context. This will result in improved user experience and as a bonus improve our developer experience as well.
Think the speed of your website doesn’t matter? Think again. A one-second delay in page load time yields: 11% fewer page views 16% decrease in customer satisfaction 7% loss in conversions A few extra seconds could have a huge impact on your ability to engage visitors and make sales. This means that having a fast site is essential — not just for ranking well with Google, but for keeping your bottom-line profits high. How speed influences conversions Slow speeds kill conversions. In fact, 47% of consumers expect websites to load in two seconds or less — and 40% will abandon a page…
As a savvy marketer, it’s our sincere hope you never start a campaign without a dedicated landing page for sending your paid traffic to. But — as you know — the job isn’t over once a landing page is created.
Your real opportunity is in understanding how your page performs.
Beyond tracking standard performance measures like conversions and landing page quality (LPQ), you’ve likely wondered about other factors like:
Is my landing page copy clear? Are there too many words? Too few?
Is my page faring well on mobile? Does it load fast enough?
Is this page just designed nicely, or is it also optimized for SEO?
Is this a good conversion rate for this type of page in my industry?
Ultimately you want to know whether you’ve got an especially high converting page, or if there’s anything specific you can improve. But it can be difficult to know what ‘good’ looks like, and you may not always have a second set of eyes to help you critique.
New: Try Unbounce’s Landing Page Analyzer
For years we’ve seen the need for a landing page audit tool or landing page grader of some sort, and so—after many months of development—we’re very pleased to unveil the Unbounce Landing Page Analyzer.
With this grader-style tool, you input your landing page URL (along with a few key details) and The Analyzer instantly delivers a comprehensive, personalized report with custom recommendations you can try today to increase your conversion rates.
Unlike other landing page reviews, The Analyzer is truly a deep dive into your performance.
Not only do you get a summary of how your page compares to others in your industry, but you also see important page performance insights including your landing page’s speed, load time, and page requests that may be slowing things down.
If The Analyzer discovers your images are too large (contributing to slow load time), your custom report will include compressed versions of all your images to replace quickly and get your page loading even faster.
Pictured: you’ll get custom, compressed images as part of your page analysis.
In The Analyzer’s comprehensive report, you’ll see specifics across nine categories, and discover whether your landing page:
Conveys trust and security
Appears properly on social networks and mobile
Is designed in a way that’s especially high converting
Contains too many calls to action
Has an appropriate Flesch reading ease and sentiment for your industry,
and much, much more.
Evaluate your landing page to reveal rea, data-backed insights in minutes.
Wait, aren’t there other landing page graders out there?
Touche! There are other landing page analyzers/graders/calculators available, but we can confidently say Unbounce’s is the most sophisticated and comprehensive you’ll find. Ours is the only landing page analyzer on the market leveraging AI technology, and the endless amount of campaign research done by our customers and our in-house marketing team.
For the past eight years, we’ve been obsessed with the question “what’s a good conversion rate?”, and Unbounce’s internal research team has employed proprietary AI technology to analyze the behavior of over 75 million visitors to 65,000 landing pages with a goal of understanding what makes a customer convert.
We have more data than any other conversion platform to provide insights on what a high-performing landing page looks like, and The Analyzer leverages this insight.
The Analyzer’s data is sourced from Google Page Speed Insights, and our very own proprietary data broken down by industry.
Actionable feedback you can implement today
The best thing about this landing page review? You’ll discover instant improvements that might take you only minutes to fix.
The Wizard of Moz himself, Rand Fishkin ran the following product’s landing page from Moz.com through The Analyzer and had some great things to discover.
How’d this Moz page fare? Here are Rand’s initial thoughts:
“I’m glad to see we passed so many of the technical checks! I was a little nervous. [I] Realized that the page is missing testimonials or social proof. That’s a head-smacking moment.”
Rand may be a bit self-depreciating here, however. Moz’s page scored really well with a 75% overall:
Rand’s overall landing page grade.
Rand’s verdict on trying out The analyzer?
“I’ve never seen a page analysis tool that’s focused on optimization. In my opinion, this can be hugely helpful for folks to quickly check that they’ve nailed the basics of landing page optimization and accessibility. I have no doubt tens of thousands of websites can get better just by applying this tool’s advice.”
What did we learn?
Interested in what The Analyzer could teach us about our in-house landing pages at Unbounce, we ran our recent event landing page for PPC Week through to see what we’d take away:
Pictured: The landing page for PPC week we input into the Landing Page Analyzer.
We learned the page converts very well for our industry (7.7%), and while the page loads pretty quickly (0.7 seconds), at 3.32MB it’s overweight and could be loading even quicker if we reduce it to less than 3MB:
Our PPC Week page’s overall grade. Note our message match and page speed could use some work.
Our PPC Week landing page is running a little slowly.
Fortunately, The Analyzer also provided us with some compressed images that will help us load up to 9% faster:
We also saw that our page title, meta description and H1 tags were helping our SEO visibility (which was important for this particular page).
All of these quick-to-change factors can improve this PPC Week page for us, but we’re most excited to see what you’ll discover about your own landing pages. Bonus, you don’t need an Unbounce-built page to try The Analyzer, either. Give it a try today and let us know what you think!
(This is a sponsored post). Websites with long or infinite scrolling are becoming more and more common lately, and it’s no mere trend or coincidence. The technique of long scrolling allows users to traverse chunks of content without any interruption or additional interaction — information simply appear as the user scrolls down the page.
Infinite scrolling is a variety of long scrolling that allows users to scroll through a massive chunk of content with no finish line in sight (it’s the endless scrolling you see on Facebook, Twitter and Tumblr feeds).
For some time, we’ve run up against the limits of what CSS can do. Those who build responsive layouts will freely admit the frustrations and shortcomings of CSS that force us to reach for CSS preprocessors, plugins and other tools to help us write the styles that we’re unable to write with CSS alone. Even still, we run into limitations with what current tools help us accomplish.
Think for a moment of a physical structure.
What would a page look like if it had no designer? This odd question occurred to me in the 1980s, while overseeing the transition from lead-based typesetting to phototypesetting of an Indian newspaper. The Patriot’s distinctive design seemed to emerge, not from a designer, but the tactile interaction between lead and the illiterate villager who assembled the pages. This article examines how design has changed as materials have evolved, and underlines how the need for deliberate design is greater than it has ever been.
I like to think of WordPress as the gateway drug of web development. Many people who get started using the platform are initially merely looking for a comfortable (and free) way to create a simple website. Some Googling and consultation of the WordPress Codex later, it’s done and that should be it. Kind of like “I’m just going to try it once.” [Links checked March/29/2017]
However, a good chunk of users don’t stop there.
Today Smashing Magazine turns eight years old. Eight years is a long time on the web, yet for us it really doesn’t feel like a long journey at all. Things have changed, evolved and moved on, and we gratefully take on new challenges one at a time. To mark this special little day, we’d love to share a few things that we’ve learned over the last year about the performance challenges of this very website and about the work we’ve done recently.
The layout is the foundation of your website. It guides the user through the sections and tells them what is most important. It also sets the aesthetic of the website. Therefore, you need to carefully think through how you lay out content.
An original, creative layout goes a long way to improving the user experience of a website, although not letting your creativity get in the way of usability is important.