Dynamic websites and single-page applications

  • Updated
  • Optimizely Web Experimentation
  • Optimizely Performance Edge
  • Optimizely Personalization

Many sites are built as single-page applications (SPAs) to optimize the speed of content delivery and improve overall site performance. SPAs offer a few key advantages over traditional static websites: they are fast, responsive, and compact. If you hear that your site is built on a framework such as React, Vue, AngularJS, Ember, or Backbone, you are likely working with a SPA.

Because SPAs load content differently from traditional static sites, Optimizely lets you build experiments on SPAs. These features support dynamic websites by letting you trigger experiments and campaigns based on changes that happen on a page, even when the page does not reload. Jump to the bottom for a more detailed explanation.

Enable support for dynamic websites to:

  • Activate experiments based on content that is not immediately displayed when the page loads or content that becomes visible based on your visitors’ navigation of your site. SPAs do not reload the entire page when a visitor navigates the site. Instead, existing elements are replaced by new ones as the visitor explores.
  • Apply or reapply changes to elements on your page that display after the initial page load. Some SPAs use infinite scroll to add elements to the page, without fully reloading the page.

Enable support for dynamic websites

To gain access to features that let you experiment on an SPA, enable support for dynamic websites. You gain the following options for activating pages:

  • When the URL changes – Triggers when the URL in the visitor's browser changes.
  • When the DOM Changes – Triggers when any element on the page changes.

These triggers define conditions under which the page will activate or deactivate. When the page activates, your experiment or campaign will run.

To enable support for dynamic websites at the project level:

  1. Go to Settings > Implementation.
  2. Select Enable Support for Dynamic Websites.

Instead of polling for elements on the site for two seconds after the initial page load, Optimizely uses MutationObserver to determine when something changes on your site to apply Visual Editor changes. This will not have a negative impact on current experiments.

Optimizely Performance Edge does not support custom snippets

Activate a page for a SPA

When you enable support for dynamic websites in your project, you can access a few additional options in your page settings.

  1. Create a page by going to Implementation > Pages > Create New Page.
  2. Note a set of triggers and conditions under Page Settings to specify when your page activates.

Triggers

Triggers tell Optimizely when to start checking whether certain conditions are true:

  • When the URL changes – Triggers activation of this page each time it sees the URL changing, even if the full page does not reload. This works well with applications that rely on client-side routing to handle user navigation. This is triggered on the initial page load and subsequent URL changes.
  • When the DOM changes – Triggers activation of this page each time an element on the page is inserted, removed, or modified.

If you have a single-page application, you are likely to use the two triggers mentioned previously most of the time. However, you still have access to the standard page activation triggers.

When you decide when Optimizely should check for certain conditions on your page, determine whether it should check if any or all conditions are true.

Conditions

Conditions tell Optimizely what criteria to check for when deciding whether to activate the page:

  • URL Match – Use URL Match Types to define where the experiment should run.
  • Element is present – Add one or more CSS selectors to check for each time the trigger for this page fires. Optimizely uses document.querySelectorAll to determine whether the element exists at the time the page fires.
  • JavaScript Condition – Add a custom JavaScript function that is evaluated when the trigger for this page fires. The function should return a Boolean value.

Add and remove multiple conditions to define the exact set of conditions under which you want your page to activate. This will determine when an experiment runs.

Under Triggers, you can tell Optimizely to check whether any conditions are true. This joins separate conditions with an OR statement. When you tell Optimizely to check that all conditions are true, conditions are joined with an AND statement.

When the conditions return as true, the page activates, and your experiment runs.

Deactivate a page

When a page is activated and the experiment runs, the visual changes in your experiment are applied to your website. Visitors who see the site under the conditions you defined and who are bucketed into the experiment see those changes live in the world. Support for dynamic websites ensures that those changes are continually applied, even as your visitors navigate around the site and conditions change. The changes persist when the page is activated, regardless of what is happening on the page.

Occasionally, you may not want the changes in your experiment to persist indefinitely as visitors navigate your site. Imagine that you have a page offering phones for sale, and on that page your experiment displays a banner advertising a promotion on a certain brand. When a user goes to a page displaying a different phone brand, however, you do not want the promotional banner to display. By setting conditions you can trigger deactivation of the banner by deactivating the page that contained it when a user navigates away from the page.

To deactivate a page when the conditions are no longer met:

  1. On the Edit Page window, go to Page Settings > Advanced.
  2. Select the Deactivation and Undo checkbox.
    Optimizely will deactivate the page and stop applying changes to that page when:
    • A trigger on the page fires
    • The conditions evaluate to false
  3. If you also want to remove previously applied changes when the page deactivates, select Undo changes when this page deactivates:
    • Changes made with the Visual Editor are removed
    • Variation CSS is reverted
    • A template’s “Reset JS” is executed
    • Templates are only available for Web and Personalization projects.
    • Custom JavaScript cannot be reverted

The next time the page trigger is fired, the page will deactivate.

If a change is made to the body tag using the visual editor, you cannot remove those changes as outlined in this article. If this is the functionality you require, submit a feature request.

How support for dynamic websites works

Changes apply to your site when a page is activated. If you have a traditional static site (and you are not using the feature kit that supports dynamic websites), Optimizely applies changes to elements on your page that load within two seconds of the initial page load. Ordinarily, this is plenty of time for elements to load so that visual changes seamlessly apply without affecting your visitors' experience.

However, unlike static sites, SPAs do not fully reload the page to change the content that a visitor sees. Instead, elements are changed or added to alter the site experience as a visitor explores. This means that two seconds is often not enough time to capture when an element loads on a SPA to apply the visual changes through an experiment.

Optimizely supports dynamic websites by monitoring when page elements load, even when the page itself does not fully reload, through a Web API called a MutationObserver.

MutationObserver provides hooks into DOM mutations and enables Optimizely Web Experimentation’s snippet to know when a DOM node is inserted, destroyed, or modified to apply (or reapply) changes at the right moment. Instead of polling for elements on the site for two seconds after the initial page load, Optimizely uses MutationObserver to see when something has changed on your site.

When an experiment or campaign activates, and a visitor is bucketed into the experiment, MutationObserver checks the DOM while the page is active, and applies (and reapplies) changes as appropriate. When the page deactivates, Optimizely no longer attempts to change the elements on that page.

The observerSelector utility function acts as a wrapper for the MutationObserver Web API.

Optimizely Web Experimentation has different levels of support for changes:

  • Insert HTML and insert image changes, which are only reapplied when new elements display
  • Rearrange changes that are never reapplied
  • Everything else, which is reapplied when new elements display and when previously modified elements are mutated

Optimizely Web Experimentation does not currently support rearrange tests (tests that swap the positions of nodes in a document) on dynamic websites, but may extend support in the future.

Dynamic selectors

Target dynamically generated selectors using custom attributes, if available. This lets you use the Visual Editor to target specific attributes for your SPA more reliably and with a lower configuration time. See Target dynamic selectors for more information.

Utility functions

Dynamic sites and SPAs often update or re-render content without a full page reload. This can cause timing issues where elements needed by an experiment are not present when the variation code runs. To address this, use Optimizely’s utility functions (accessed via window.optimizely.get('utils')) to wait for elements to appear or disappear before executing your code. You can use these functions to:

  • Recursively reapply variation code whenever changes occur.
  • Reactivate experiments when a specific condition is met, such as the appearance of an element or a user interaction.

Utility functions overview

Optimizely provides utility methods like waitUntil and waitForElement to help you manage asynchronous DOM changes. These methods ensure that your variation code or experiment activation only occurs when the appropriate DOM state is reached. Learn more about Utils.

  • utils.waitUntil(condition) – Continuously checks a provided condition until it returns true. When the condition is met, it resolves a promise. Use this to detect when an element is removed from or displayed in the DOM.

  • utils.waitForElement(selector) – Waits for a specific DOM element (matched by a CSS selector) to be present before proceeding. This method is ideal for reacting to changes such as dynamic element insertion.

Recursively reapplying variation code

The following code demonstrates how to apply variation code that injects an element and then continuously monitors the DOM. If the target element (an element with the class .added-element) is removed, possibly due to the site re-rendering the page, the code re-checks the condition and re-applies the variation. You can find the code example here.

The following explains how the code works:

  1. Initial application – Execute the variationCode function to insert a new <div>; element into the DOM.
  2. Set up the watch – Immediately after insertion, call applyOnRerender with the CSS selector for the new element. This function uses utils.waitUntil to monitor the DOM.
  3. Reapply on rerender – When the targeted element no longer exists (e.g., if the dynamic page re-renders and removes the element), the promise resolves. If the URL still matches the condition, execute variationCode again, ensuring that the variation persists as long as necessary.

Callback page targeting for experiment activation

Sometimes, you need to re-trigger experiment activation based on a change in the DOM or user interaction. The following examples demonstrate two methods for reactivating your experiment:

Method 1: Waiting for an element to disappear

Wait for a specific element, such as .some-element, to be removed from the DOM. Once the element is gone and the URL condition is met, call the activation function to reinitialize the experiment.

function trigger(activate, options) {
   var utils = window.optimizely.get('utils');
   if (window.location.href.indexOf(/* Some URL */) !== -1) {
       utils.waitUntil(function() {
           return !!document.querySelector('.some-element');
       }).then(function() {
           // If the experiment is active, deactivate it before reactivating.
           options.isActive && activate({isActive: false});
       });
   }
}

Method 2: Waiting for an element and user interaction

Wait for a specific element, like a button, to display and attach an event listener that triggers the experiment activation when the button is clicked. This approach is useful when user actions determine when to reapply variation code.

function trigger(activate, options) {
    var utils = window.optimizely.get('utils');

    if (window.location.href.indexOf(/* Some URL */) !== -1) {
        utils.waitForElement('button').then(function(buttonElement) {
            buttonElement.addEventListener('click', function() {
                // Optionally, deactivate the experiment first if it's already active
               options.isActive && activate({isActive: false});
                // Activate the experiment.
                activate();
            });
        });
    }
}

Key points

  • URL condition – Ensure the current URL contains a specific substring. This condition makes sure the experiment is only triggered on the correct page or context.
  • Reactivation logic – Reset the activation state depending on your experiment’s requirements, (isActive: false) before reapplying the variation code.
  • User interaction – Focus on triggering reactivation through user actions, such as clicking a button, which is especially useful for interactive or multi-step experiments.

Best practices and conditions

  • Performance – Avoid overusing utility functions to prevent excessive DOM polling. Ensure the conditions in waitUntil or waitForElement are specific to avoid unnecessary reactivation cycles.
  • URL matching – Use a simple index check on window.location.href to determine if the experiment should run. Depending on your use case, consider using more robust URL matching or regular expressions for better control.
  • Cleanup – Ensure that your experiments include cleanup procedures if they add significant DOM changes or event listeners, especially when experiments are deactivated or users navigate away.
  • Quality assurance – Test these implementations across various states of your SPA to confirm that the variation code behaves as expected under different dynamic conditions.