diff --git a/src/Plugin/Field/FieldFormatter/AdvancedTextFormatter.php b/src/Plugin/Field/FieldFormatter/AdvancedTextFormatter.php
index 9751b5a..3ebac7d 100644
--- a/src/Plugin/Field/FieldFormatter/AdvancedTextFormatter.php
+++ b/src/Plugin/Field/FieldFormatter/AdvancedTextFormatter.php
@@ -4,9 +4,12 @@ namespace Drupal\advanced_text_formatter\Plugin\Field\FieldFormatter;
 
 use Drupal\Component\Utility\Xss;
 use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\Field\FieldItemInterface;
 use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Render\BubbleableMetadata;
 
 /**
  * Plugin implementation of the 'advanced_text_formatter' formatter.
@@ -258,61 +261,178 @@ class AdvancedTextFormatter extends FormatterBase {
    */
   public function viewElements(FieldItemListInterface $items, $langcode) {
     $elements = [];
+    $entity = $items->getEntity();
+
     $token_data = [
       'user' => \Drupal::currentUser(),
-      $items->getEntity()->getEntityTypeId() => $items->getEntity(),
+      $entity->getEntityTypeId() => $entity,
     ];
 
     foreach ($items as $delta => $item) {
-      if ($this->getSetting('use_summary') && !empty($item->summary)) {
-        $output = $item->summary;
+      // We need to determine whether this item should use a lazy builder or
+      // not. This depends on whether there is token replacement and what the
+      // token replacement does to the cacheability, and so we need to perform
+      // the token replacement first, even if we're not going to use the result
+      // here.
+      $should_use_lazy_builder = FALSE;
+      if ($this->getSetting('token_replace')) {
+        if ($this->getSetting('use_summary') && !empty($item->summary)) {
+          $output = $item->summary;
+        }
+        else {
+          $output = $item->value;
+        }
+
+        $bubbleable_metadata = new BubbleableMetadata();
+
+        $output = \Drupal::token()->replace($output, $token_data, [], $bubbleable_metadata);
+
+        // Use a dummy render array to check whether to use a lazy builder.
+        $dummy_element = [];
+        $bubbleable_metadata->applyTo($dummy_element);
+        $placeholderGenerator = \Drupal::service('render_placeholder_generator');
+        $should_use_lazy_builder = $placeholderGenerator->shouldAutomaticallyPlaceholder($dummy_element);
+      }
+
+      if ($should_use_lazy_builder) {
+        // Use a lazy builder for the element. The lazy builder callback will
+        // call viewElement().
+        $element = [
+          '#lazy_builder' => [
+            static::class . '::viewElementLazyBuilder',
+            [
+              $entity->getEntityTypeId(),
+              $entity->id(),
+              $this->fieldDefinition->getName(),
+              $item->getLangcode(),
+              $this->viewMode,
+              $delta,
+            ],
+          ],
+          // Use the cache data from the dummy element. We can't use the whole
+          // of that element, as applyTo() adds the '#attached' property, which
+          // the lazy builder must not have.
+          '#cache' => $dummy_element['#cache'],
+        ];
       }
       else {
-        $output = $item->value;
+        $element = $this->viewElement($item);
       }
 
-      if ($this->getSetting('token_replace')) {
-        $output = \Drupal::token()->replace($output, $token_data);
-      }
+      $elements[$delta] = $element;
+    }
+
+    return $elements;
+  }
+
+  /**
+   * Lazy builder callback.
+   *
+   * @param string $entity_type_id
+   *  The entity type ID.
+   * @param int $entity_id
+   *  The entity ID.
+   * @param string $fieldname
+   *  The name of the field this formatter is on.
+   * @param string $langcode
+   *  The language of the item being displayed.
+   * @param string $view_mode
+   *  The view mode being used.
+   * @param int $delta
+   *  The delta of the item.
+   *
+   * @return array
+   *  The render array.
+   */
+  public static function viewElementLazyBuilder($entity_type_id, $entity_id, $fieldname, $langcode, $view_mode, $delta) {
+    // Get an instance of this formatter, configured for the entity, field,
+    // and view mode.
+    $entity = \Drupal::service('entity_type.manager')->getStorage($entity_type_id)->load($entity_id);
+    // Make sure we use the translated entity where appropriate.
+    if ($entity->hasTranslation($langcode)) {
+      $entity = $entity->getTranslation($langcode);
+    }
+    $item = $entity->get($fieldname)[$delta];
+    $view_displays = EntityViewDisplay::collectRenderDisplays([$entity], $view_mode); // ugly, as does querying.
+    $view_display = $view_displays[$entity->bundle()];
+    $formatter = $view_display->getRenderer($fieldname);
+    return $formatter->viewElement($item);
+  }
+
+  /**
+   * Creates the render array for a single element.
+   *
+   * This acts as a callback for the lazy builder when one is needed.
+   *
+   * @param \Drupal\Core\Field\FieldItemInterface $item
+   *   The field item.
+   *
+   * @return array
+   *   The render array for the item. Note that this is nested in order to allow
+   *   it to function as the return value for a lazy builder callback.
+   */
+  public function viewElement(FieldItemInterface $item) {
+    $element = [];
 
-      switch ($this->getSetting('filter')) {
-        case static::FORMAT_DRUPAL:
-          $output = check_markup($output, $this->getSetting('format'), $item->getLangcode());
+    if ($this->getSetting('use_summary') && !empty($item->summary)) {
+      $output = $item->summary;
+    }
+    else {
+      $output = $item->value;
+    }
 
-          break;
+    if ($this->getSetting('token_replace')) {
+      $entity = $item->getEntity();
 
-        case static::FORMAT_PHP:
-          $output = Xss::filter($output, $this->getSetting('allowed_html'));
+      $token_data = [
+        'user' => \Drupal::currentUser(),
+        $entity->getEntityTypeId() => $entity,
+      ];
 
-          if ($this->getSetting('autop')) {
-            $output = _filter_autop($output);
-          }
+      $bubbleable_metadata = new BubbleableMetadata();
+      $output = \Drupal::token()->replace($output, $token_data, [], $bubbleable_metadata);
+      $bubbleable_metadata->applyTo($element);
+    }
 
-          break;
+    switch ($this->getSetting('filter')) {
+      case static::FORMAT_DRUPAL:
+        $output = check_markup($output, $this->getSetting('format'), $item->getLangcode());
 
-        case static::FORMAT_INPUT:
-          $output = check_markup($output, $item->format, $item->getLangcode());
+        break;
 
-          break;
-      }
+      case static::FORMAT_PHP:
+        $output = Xss::filter($output, $this->getSetting('allowed_html'));
 
-      if ($this->getSetting('trim_length') > 0) {
-        $options = [
-          'word_boundary' => $this->getSetting('word_boundary'),
-          'max_length'    => $this->getSetting('trim_length'),
-          'ellipsis'      => $this->getSetting('ellipsis'),
-        ];
+        if ($this->getSetting('autop')) {
+          $output = _filter_autop($output);
+        }
 
-        $output = advanced_text_formatter_trim_text($output, $options);
-      }
+        break;
+
+      case static::FORMAT_INPUT:
+        $output = check_markup($output, $item->format, $item->getLangcode());
+
+        break;
+    }
 
-      $elements[$delta] = [
-        '#markup' => $output,
-        '#langcode' => $item->getLangcode(),
+    if ($this->getSetting('trim_length') > 0) {
+      $options = [
+        'word_boundary' => $this->getSetting('word_boundary'),
+        'max_length'    => $this->getSetting('trim_length'),
+        'ellipsis'      => $this->getSetting('ellipsis'),
       ];
+
+      $output = advanced_text_formatter_trim_text($output, $options);
     }
 
-    return $elements;
+    $element['item'] = [
+      '#type' => 'processed_text',
+      '#text' => $output,
+      '#format' => $this->getSetting('format'),
+      '#langcode' => $item->getLangcode(),
+    ];
+
+    return $element;
   }
 
 }
