diff --git a/admin_menu.module b/admin_menu.module
index 4ee6f75..e715dd6 100644
--- a/admin_menu.module
+++ b/admin_menu.module
@@ -65,6 +65,7 @@ function admin_menu_menu() {
   // @see http://drupal.org/project/js
   $items['js/admin_menu/cache'] = array(
     'page callback' => 'admin_menu_js_cache',
+    'delivery callback' => 'admin_menu_deliver',
     'access arguments' => array('access administration menu'),
     'type' => MENU_CALLBACK,
   );
@@ -299,48 +300,59 @@ function admin_menu_cache_set($cid, $data) {
 
 /**
  * Menu callback; Output administration menu for HTTP caching via AJAX request.
+ *
+ * @see admin_menu_deliver()
  */
-function admin_menu_js_cache($hash = NULL) {
-  // Get the rendered menu.
-  $content = admin_menu_output();
-
-  // @todo According to http://www.mnot.net/blog/2006/05/11/browser_caching,
-  //   IE will only cache the content when it is compressed.
-  // Determine if the client accepts gzipped data.
-  if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
-    if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== FALSE) {
-      $encoding = 'x-gzip';
-    }
-    elseif (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) {
-      $encoding = 'gzip';
-    }
-    // Perform gzip compression when:
-    // 1) the user agent supports gzip compression.
-    // 2) Drupal page compression is enabled. Sites may wish to perform the
-    //    compression at the web server level (e.g. using mod_deflate).
-    // 3) PHP's zlib extension is loaded, but zlib compression is disabled.
-    if (isset($encoding) && variable_get('page_compression', TRUE) && extension_loaded('zlib') && zlib_get_coding_type() === FALSE) {
-      // Use Vary header to tell caches to keep separate versions of the menu
-      // based on user agent capabilities.
-      header('Vary: Accept-Encoding');
-      // Since we manually perform compression, we are also responsible to
-      // send a proper encoding header.
-      header('Content-Encoding: ' . $encoding);
-      $content = gzencode($content, 9, FORCE_GZIP);
-    }
+function admin_menu_js_cache() {
+  global $conf;
+
+  // Enforce page caching.
+  $conf['cache'] = 1;
+  drupal_page_is_cacheable(TRUE);
+
+  // If we have a cache, serve it.
+  // @see _drupal_bootstrap_page_cache()
+  $cache = drupal_page_get_cache();
+  if (is_object($cache)) {
+    header('X-Drupal-Cache: HIT');
+    // Restore the metadata cached with the page.
+    $_GET['q'] = $cache->data['path'];
+    //drupal_set_title($cache->data['title'], PASS_THROUGH);
+    date_default_timezone_set(drupal_get_user_timezone());
+
+    drupal_serve_page_from_cache($cache);
+
+    // We are done.
+    exit;
   }
 
+  // Otherwise, create a new.
+  header('X-Drupal-Cache: MISS');
+
   $max_age = 3600 * 24 * 365;
-  header('Expires: ' . gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
-  header('Last-Modified: ' . gmdate(DATE_RFC1123, REQUEST_TIME));
-  header('Cache-Control: max-age=' . $max_age . ', private');
-  header('Content-Length: ' . drupal_strlen($content));
-  header('Content-Type: text/html; charset=utf-8');
-
-  // Suppress Devel module.
-  $GLOBALS['devel_shutdown'] = FALSE;
-  echo $content;
-  exit;
+  drupal_add_http_header('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
+  drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
+
+  // Return the rendered menu.
+  return admin_menu_output();
+}
+
+/**
+ * Delivery callback for client-side HTTP caching.
+ *
+ * @see admin_menu_js_cache()
+ */
+function admin_menu_deliver($page_callback_result) {
+  drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
+
+  // Send appropriate language header for browsers.
+  global $language;
+  drupal_add_http_header('Content-Language', $language->language);
+
+  print $page_callback_result;
+
+  // Perform end-of-request tasks. Page caching happens here.
+  drupal_page_footer();
 }
 
 /**
@@ -409,16 +421,16 @@ function admin_menu_output() {
   // Do nothing at all here if the client supports client-side caching, the user
   // has a hash, and is NOT requesting the cache update path. Consult the hash
   // cache last, since it requires a DB request.
-  // @todo Implement a sanity-check to prevent permanent double requests; i.e.
-  //   what if the client-side cache fails for any reason and performs a second
-  //   request on every page?
-  if (!empty($_COOKIE['has_js']) && strpos($_GET['q'], 'js/admin_menu/cache') !== 0) {
+  // @todo admin_menu_output() is only called from hook_page_build() and
+  //   admin_menu_js_cache() now. This can be moved into hook_page_build().
+  if (!empty($_COOKIE['has_js']) && strpos($_GET['q'], 'js/admin_menu/cache') === FALSE) {
     if (admin_menu_cache_get($cid)) {
       return;
     }
   }
 
   // Try to load and output administration menu from server-side cache.
+  // @todo Obsolete? Could re-use the page cache?
   if ($cache_server_enabled) {
     $cache = cache_get($cid, 'cache_menu');
     if ($cache && isset($cache->data)) {
