diff --git a/core/core.services.yml b/core/core.services.yml
index 317af02b89a3ade3a90fb24df0c79d1f019bfc01..c158be466098b3a798fb06a4cc2a56f8757065ea 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -278,6 +278,11 @@ services:
     arguments: ['@database', '@router.builder']
     tags:
       - { name: event_subscriber }
+  router.route_preloader:
+    class: Drupal\Core\Routing\RoutePreloader
+    arguments: ['@router.route_provider', '@state', '@content_negotiation']
+    tags:
+      - { name: 'event_subscriber' }
   router.matcher.final_matcher:
     class: Drupal\Core\Routing\UrlMatcher
   router.matcher:
diff --git a/core/lib/Drupal/Core/Routing/RoutePreloader.php b/core/lib/Drupal/Core/Routing/RoutePreloader.php
new file mode 100644
index 0000000000000000000000000000000000000000..daa41301477890fb5abd3450955eec16df33a574
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/RoutePreloader.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Routing\RoutePreloader.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Drupal\Core\ContentNegotiation;
+use Drupal\Core\KeyValueStore\StateInterface;
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\KernelEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Defines a class which preloads non-admin routes.
+ *
+ * On an actual site we want to avoid too many database queries so we build a
+ * list of all routes which most likely appear on the actual site, which are all
+ * HTML routes not starting with "/admin".
+ */
+class RoutePreloader implements EventSubscriberInterface {
+
+  /**
+   * The route provider.
+   *
+   * @var \Drupal\Core\Routing\RouteProviderInterface
+   */
+  protected $routeProvider;
+
+  /**
+   * The state key value store.
+   *
+   * @var \Drupal\Core\KeyValueStore\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The content negotiation.
+   *
+   * @var \Drupal\Core\ContentNegotiation
+   */
+  protected $negotiation;
+
+  /**
+   * Contains the non-admin routes while rebuilding the routes.
+   *
+   * @var array
+   */
+  protected $nonAdminRoutesOnRebuild = array();
+
+  /**
+   * Constructs a new RoutePreloader.
+   *
+   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
+   *   The route provider.
+   * @param \Drupal\Core\KeyValueStore\StateInterface $state
+   *   The state key value store.
+   * @param \Drupal\Core\ContentNegotiation $negotiation
+   *   The content negotiation.
+   */
+  public function __construct(RouteProviderInterface $route_provider, StateInterface $state, ContentNegotiation $negotiation) {
+    $this->routeProvider = $route_provider;
+    $this->state = $state;
+    $this->negotiation = $negotiation;
+  }
+
+  /**
+   * Loads all non-admin routes right before the actual page is rendered.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\KernelEvent $event
+   *   The event to process.
+   */
+  public function onRequest(KernelEvent $event) {
+    // Just preload on normal HTML pages, as they will display menu links.
+    if ($this->negotiation->getContentType($event->getRequest()) == 'html') {
+      $this->loadNonAdminRoutes();
+    }
+  }
+
+  /**
+   * Load all the non-admin routes at once.
+   */
+  protected function loadNonAdminRoutes() {
+    if ($routes = $this->state->get('routing.non_admin_routes', array())) {
+      $this->routeProvider->getRoutesByNames($routes);
+    }
+  }
+
+  /**
+   * Alters existing routes for a specific collection.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The route build event.
+   */
+  public function onAlterRoutes(RouteBuildEvent $event) {
+    $collection = $event->getRouteCollection();
+    foreach ($collection->all() as $name => $route) {
+      if (strpos($route->getPath(), '/admin/') !== 0 && $route->getPath() != '/admin') {
+        $this->nonAdminRoutesOnRebuild[] = $name;
+      }
+    }
+    $this->nonAdminRoutesOnRebuild = array_unique($this->nonAdminRoutesOnRebuild);
+  }
+
+  /**
+   * Store the non admin routes in state when the route building is finished.
+   *
+   * @param \Symfony\Component\EventDispatcher\Event $event
+   *   The route finish event.
+   */
+  public function onFinishedRoutes(Event $event) {
+    $this->state->set('routing.non_admin_routes', $this->nonAdminRoutesOnRebuild);
+    $this->nonAdminRoutesOnRebuild = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Set a really low priority to catch as many as possible routes.
+    $events[RoutingEvents::ALTER] = array('onAlterRoutes', -1024);
+    $events[RoutingEvents::FINISHED] = array('onFinishedRoutes');
+    // Load the routes before the controller is executed (which happens after
+    // the kernel request event).
+    $events[KernelEvents::REQUEST][] = array('onRequest');
+    return $events;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php b/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..334e9039bd30485ecdb01ef423edcfa1033880f4
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php
@@ -0,0 +1,192 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Routing\RoutePreloaderTest.
+ */
+
+namespace Drupal\Tests\Core\Routing;
+
+use Drupal\Core\Routing\RoutePreloader;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Tests the non admin routes preloader.
+ *
+ * @see \Drupal\Core\Routing\RoutePreloader
+ */
+class RoutePreloaderTest extends UnitTestCase {
+
+  /**
+   * The mocked route provider.
+   *
+   * @var \Drupal\Core\Routing\RouteProviderInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $routeProvider;
+
+  /**
+   * The mocked state.
+   *
+   * @var \Drupal\Core\KeyValueStore\StateInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $state;
+
+  /**
+   * The mocked content negotiator.
+   *
+   * @var \Drupal\Core\ContentNegotiation|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $negotiation;
+
+  /**
+   * The tested preloader.
+   *
+   * @var \Drupal\Core\Routing\RoutePreloader
+   */
+  protected $preloader;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Route preloader',
+      'description' => 'Tests the non admin routes preloader.',
+      'group' => 'Routing',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
+    $this->state = $this->getMock('\Drupal\Core\KeyValueStore\StateInterface');
+    $this->negotiation = $this->getMockBuilder('\Drupal\Core\ContentNegotiation')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->preloader = new RoutePreloader($this->routeProvider, $this->state, $this->negotiation);
+  }
+
+  /**
+   * Tests onAlterRoutes with just admin routes.
+   */
+  public function testOnAlterRoutesWithAdminRoutes() {
+    $event = $this->getMockBuilder('Drupal\Core\Routing\RouteBuildEvent')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $route_collection = new RouteCollection();
+    $route_collection->add('test', new Route('/admin/foo', array('_content' => 'Drupal\ExampleController')));
+    $route_collection->add('test2', new Route('/admin/bar', array('_content' => 'Drupal\ExampleController')));
+    $event->expects($this->once())
+      ->method('getRouteCollection')
+      ->will($this->returnValue($route_collection));
+
+    $this->state->expects($this->once())
+      ->method('set')
+      ->with('routing.non_admin_routes', array());
+    $this->preloader->onAlterRoutes($event);
+    $this->preloader->onFinishedRoutes(new Event());
+  }
+
+  /**
+   * Tests onAlterRoutes with "admin" appearing in the path.
+   */
+  public function testOnAlterRoutesWithAdminPathNoAdminRoute() {
+    $event = $this->getMockBuilder('Drupal\Core\Routing\RouteBuildEvent')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $route_collection = new RouteCollection();
+    $route_collection->add('test', new Route('/foo/admin/foo', array('_content' => 'Drupal\ExampleController')));
+    $route_collection->add('test2', new Route('/bar/admin/bar', array('_content' => 'Drupal\ExampleController')));
+    $route_collection->add('test3', new Route('/administrator/a', array('_content' => 'Drupal\ExampleController')));
+    $route_collection->add('test4', new Route('/admin', array('_content' => 'Drupal\ExampleController')));
+    $event->expects($this->once())
+      ->method('getRouteCollection')
+      ->will($this->returnValue($route_collection));
+
+    $this->state->expects($this->once())
+      ->method('set')
+      ->with('routing.non_admin_routes', array('test', 'test2', 'test3'));
+    $this->preloader->onAlterRoutes($event);
+    $this->preloader->onFinishedRoutes(new Event());
+  }
+
+
+  /**
+   * Tests onAlterRoutes with admin routes and non admin routes.
+   */
+  public function testOnAlterRoutesWithNonAdminRoutes() {
+    $event = $this->getMockBuilder('Drupal\Core\Routing\RouteBuildEvent')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $route_collection = new RouteCollection();
+    $route_collection->add('test', new Route('/admin/foo', array('_content' => 'Drupal\ExampleController')));
+    $route_collection->add('test2', new Route('/bar', array('_content' => 'Drupal\ExampleController')));
+    // Non content routes, like ajax callbacks should be ignored.
+    $route_collection->add('test3', new Route('/bar', array('_controller' => 'Drupal\ExampleController')));
+    $event->expects($this->once())
+      ->method('getRouteCollection')
+      ->will($this->returnValue($route_collection));
+
+    $this->state->expects($this->once())
+      ->method('set')
+      ->with('routing.non_admin_routes', array('test2', 'test3'));
+    $this->preloader->onAlterRoutes($event);
+    $this->preloader->onFinishedRoutes(new Event());
+  }
+
+  /**
+   * Tests onRequest on a non html request.
+   */
+  public function testOnRequestNonHtml() {
+    $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\KernelEvent')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $request = new Request();
+    $event->expects($this->any())
+      ->method('getRequest')
+      ->will($this->returnValue($request));
+    $this->negotiation->expects($this->once())
+      ->method('getContentType')
+      ->will($this->returnValue('non-html'));
+
+    $this->routeProvider->expects($this->never())
+      ->method('getRoutesByNames');
+    $this->state->expects($this->never())
+      ->method('get');
+
+    $this->preloader->onRequest($event);
+  }
+
+  /**
+   * Tests onRequest on a html request.
+   */
+  public function testOnRequestOnHtml() {
+    $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\KernelEvent')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $request = new Request();
+    $event->expects($this->any())
+      ->method('getRequest')
+      ->will($this->returnValue($request));
+    $this->negotiation->expects($this->once())
+      ->method('getContentType')
+      ->will($this->returnValue('html'));
+
+    $this->routeProvider->expects($this->once())
+      ->method('getRoutesByNames')
+      ->with(array('test2'));
+    $this->state->expects($this->once())
+      ->method('get')
+      ->with('routing.non_admin_routes')
+      ->will($this->returnValue(array('test2')));
+
+    $this->preloader->onRequest($event);
+  }
+
+}