diff --git a/content_lock.libraries.yml b/content_lock.libraries.yml
index e69de29..125b726 100644
--- a/content_lock.libraries.yml
+++ b/content_lock.libraries.yml
@@ -0,0 +1,7 @@
+lock_button:
+  js:
+    js/content-lock-button.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/jquery.once
diff --git a/content_lock.module b/content_lock.module
index 143c5d2..286f2c9 100644
--- a/content_lock.module
+++ b/content_lock.module
@@ -13,6 +13,7 @@ use Drupal\Core\Url;
 use Drupal\Core\Routing\LocalRedirectResponse;
 use Drupal\Core\Entity\EntityFormInterface;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Implements hook_help().
@@ -81,41 +82,157 @@ function content_lock_form_alter(&$form, FormStateInterface $form_state, $form_i
     $form['actions']['submit']['#submit'][] = 'content_lock_form_submit';
     $form['actions']['publish']['#submit'][] = 'content_lock_form_submit';
 
-    // This hook function is called twice, first when the form loads
-    // and second when the form is submitted.
-    // Only perform set and check for lock on initial form load.
-    $userInput = $form_state->getUserInput();
-    if (!empty($userInput)) {
-      return;
+    // Directly locking the entity when building the form conflicts with the
+    // Prefetch Cache module, which caches the fully rendered form before the
+    // user ever visits the corresponding page. Thus, we provide a hidden Ajax
+    // button that will be triggered automatically via JavaScript upon loading
+    // the page.
+    $form['actions']['lock'] = [
+      '#type' => 'submit',
+      '#value' => t('Lock'),
+      '#submit' => ['content_lock_ajax_lock_submit'],
+      // When the form is disabled as part of the Ajax request, this button must
+      // not be disabled. Otherwise it would not be detected as the triggering
+      // element when rebuilding the form. Explicitly setting #disabled to FALSE
+      // prevents this element from being disabled by inheritance.
+      /* @see content_lock_process_lock_form() */
+      /* @see content_lock_ajax_lock_submit() */
+      /* @see \Drupal\Core\Form\FormBuilder::doBuildForm() */
+      '#disabled' => FALSE,
+      '#name' => 'content_lock_button',
+      '#ajax' => [
+        // Because the entire form may be disabled as part of the Ajax,
+        // we need to replace the entire form.
+        'wrapper' => $form['#id'] . '-wrapper',
+        'callback' => 'content_lock_ajax_lock_callback',
+      ],
+      '#attributes' => [
+        'class' => [
+          // Clients without JavaScript will have to click this button manually
+          // in order to lock the entity.
+          'js-hide',
+          'content-lock-button',
+        ]
+      ],
+      '#attached' => ['library' => ['content_lock/lock_button']],
+      '#weight' => 190,
+    ];
+    // Add a wrapper around the entire form.
+    $form['#prefix'] = '<div id="' . $form['#id'] . '-wrapper">';
+    $form['#suffix'] = '</div>';
+
+    // If the content was locked via Ajax and is now being rebuilt we need to
+    // remove the 'Lock' button and either add an 'Unlock' button or disable the
+    // form. This must be done in a process callback as otherwise the form token
+    // validation will fail as the 'form_token' form element will be disabled.
+    /* @see \Drupal\Core\Form\FormBuilder::doBuildForm() */
+    /* @see content_lock_process_form() */
+    if ($form_state->has('content_lock')) {
+      $form['#process'][] = 'content_lock_process_form';
     }
+  }
+}
 
-    // We lock the content if it is currently edited by another user.
-    if (!$lock_service->locking($entity->id(), $user->id(), $entity_type)) {
-      $form['#disabled'] = TRUE;
+/**
+ * Form process callback to add an 'Unlock' button or disable the form.
+ *
+ * @param $form
+ *   Nested array of form elements that comprise the form.
+ * @param $form_state
+ *   The current state of the form.
+ *
+ * @return array
+ *   The processed form.
+ */
+function content_lock_process_form(array $form, FormStateInterface $form_state) {
+  unset($form['actions']['lock']);
 
-      // Do not allow deletion, publishing, or unpublishing if locked.
-      if (isset($form['actions']['delete'])) {
-        unset($form['actions']['delete']);
-      }
+  if ($form_state->get('content_lock')) {
+    /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */
+    $lock_service = \Drupal::service('content_lock');
 
-      if (isset($form['actions']['publish'])) {
-        unset($form['actions']['publish']);
-      }
-      if (isset($form['actions']['unpublish'])) {
-        unset($form['actions']['unpublish']);
-      }
+    /** @var \Drupal\core\Entity\ContentEntityInterface $entity */
+    $entity = $form_state->getFormObject()->getEntity();
+    $entity_type = $entity->getEntityTypeId();
+    $form['actions']['unlock'] = $lock_service->unlockButton($entity_type, $entity->id(), \Drupal::request()->query->get('destination'));
+  }
+  else {
+    $form['#disabled'] = TRUE;
 
-      // If moderation state is in use also disable corresponding buttons.
-      if (isset($form['moderation_state'])) {
-        unset($form['moderation_state']);
-      }
+    // Do not allow deletion, publishing, or unpublishing if locked.
+    if (isset($form['actions']['delete'])) {
+      unset($form['actions']['delete']);
+    }
+
+    if (isset($form['actions']['publish'])) {
+      unset($form['actions']['publish']);
+    }
+    if (isset($form['actions']['unpublish'])) {
+      unset($form['actions']['unpublish']);
     }
-    else {
-      // ContentLock::locking() returns TRUE if the content is locked by the
-      // current user. Add an unlock button only for this user.
-      $form['actions']['unlock'] = $lock_service->unlockButton($entity_type, $entity->id(), \Drupal::request()->query->get('destination'));
+
+    // If moderation state is in use also disable corresponding buttons.
+    if (isset($form['moderation_state'])) {
+      unset($form['moderation_state']);
+    }
+  }
+  return $form;
+}
+
+/**
+ * Form submission callback for the 'Lock' button.
+ *
+ * @param $form
+ *   Nested array of form elements that comprise the form.
+ * @param $form_state
+ *   The current state of the form.
+ */
+function content_lock_ajax_lock_submit(array $form, FormStateInterface $form_state) {
+  /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */
+  $lock_service = \Drupal::service('content_lock');
+
+  /** @var \Drupal\core\Entity\ContentEntityInterface $entity */
+  $entity = $form_state->getFormObject()->getEntity();
+  $user = \Drupal::currentUser();
+  $entity_type = $entity->getEntityTypeId();
+
+  $locked = $lock_service->locking($entity->id(), $user->id(), $entity_type);
+  $form_state
+    ->set('content_lock', $locked)
+    ->setRebuild(TRUE);
+}
+
+/**
+ * Form Ajax callback for the 'Lock' button.
+ *
+ * @param $form
+ *   Nested array of form elements that comprise the form.
+ * @param $form_state
+ *   The current state of the form.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ *   The request that triggered the Ajax operation.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ *   The Ajax response for this request.
+ */
+function content_lock_ajax_lock_callback(array $form, FormStateInterface $form_state, Request $request) {
+  // Because we replace the entire form, messages do not show up without
+  // explicitly specifying the selector to which they should be prepended.
+  // Therefore we manually render the Ajax response and then add the selector
+  // to the command that inserts the messages.
+  /** @var \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer */
+  $ajax_renderer = \Drupal::service('main_content_renderer.ajax');
+  /** @var \Drupal\Core\Ajax\AjaxResponse $response */
+  // Render the entire form.
+  $response = $ajax_renderer->renderResponse($form, $request, \Drupal::routeMatch());
+
+  $commands = &$response->getCommands();
+  foreach ($commands as &$command) {
+    if (($command['command'] === 'insert') && ($command['method'] === 'prepend')) {
+      $command['selector'] = '#' . $form['#id'];
     }
   }
+  return $response;
 }
 
 /**
diff --git a/js/content-lock-button.js b/js/content-lock-button.js
index e69de29..1cc260a 100644
--- a/js/content-lock-button.js
+++ b/js/content-lock-button.js
@@ -0,0 +1,31 @@
+/**
+ * @file
+ * Defines Javascript behaviors for the Content Lock button.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+  /**
+   * Behaviors for tabs in the node edit form.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches automatic submission behavior for content lock buttons.
+   */
+  Drupal.behaviors.contentLockButton = {
+    attach: function attach(context) {
+      var submitButton = function() {
+        var $button = $(this);
+        window.setTimeout(function() {
+          $button.trigger('mousedown');
+        })
+      };
+
+      $(context)
+        .find('.content-lock-button')
+        .once('content-lock-button')
+        .each(submitButton);
+    }
+  };
+}(jQuery, Drupal, drupalSettings));
diff --git a/src/ContentLock/ContentLock.php b/src/ContentLock/ContentLock.php
index 6c9ab5f..ab4fddc 100644
--- a/src/ContentLock/ContentLock.php
+++ b/src/ContentLock/ContentLock.php
@@ -5,9 +5,11 @@ namespace Drupal\content_lock\ContentLock;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Extension\ModuleHandler;
 use Drupal\Core\DependencyInjection\ServiceProviderBase;
 use Drupal\Core\Access\CsrfTokenGenerator;
+use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Link;
 use Drupal\Core\Datetime\DateFormatter;
@@ -428,11 +430,29 @@ class ContentLock extends ServiceProviderBase {
         // Higher permission user can unblock.
         if ($this->currentUser->hasPermission('break content lock')) {
 
+          // The locking may have occurred as part of an Ajax request, in which
+          // case we need to remove the Ajax query parameters from the
+          // destination URL in the link.
+          $url = parse_url($this->currentRequest->getRequestUri());
+          $destination = $url['path'];
+          if (isset($url['query'])) {
+            parse_str($url['query'], $query);
+            unset($query[FormBuilderInterface::AJAX_FORM_REQUEST]);
+            if (isset($query[MainContentViewSubscriber::WRAPPER_FORMAT]) && ($query[MainContentViewSubscriber::WRAPPER_FORMAT] === 'drupal_ajax')) {
+              unset($query[MainContentViewSubscriber::WRAPPER_FORMAT]);
+            }
+            if ($query) {
+              $destination .= '?' . http_build_query($query);
+            }
+          }
+          if (isset($url['fragment'])) {
+            $destination .= '#' . $url['fragment'];
+          }
           $link = Link::createFromRoute(
             $this->t('Break lock'),
             'content_lock.break_lock.' . $entity_type,
             ['entity' => $entity_id],
-            ['query' => ['destination' => $this->currentRequest->getRequestUri()]]
+            ['query' => ['destination' => $destination]]
           )->toString();
 
           // Let user break lock.
