diff --git src/js/_enqueues/admin/common.js src/js/_enqueues/admin/common.js
index 2fc0c38828..84208a0268 100644
--- src/js/_enqueues/admin/common.js
+++ src/js/_enqueues/admin/common.js
@@ -1569,7 +1569,7 @@ $document.ready( function() {
 		 * notice. Make sure it gets moved just the first time.
 		 */
 		if ( ! $progressDiv.hasClass( 'update-details-moved' ) ) {
-			$progressDiv.insertAfter( $updateNotice ).addClass( 'update-details-moved' );
+			$progressDiv.appendTo( $updateNotice ).addClass( 'update-details-moved' );
 		}
 
 		// Toggle the progress div visibility.
diff --git src/wp-admin/css/common.css src/wp-admin/css/common.css
index 868978d55f..e475c0aa54 100644
--- src/wp-admin/css/common.css
+++ src/wp-admin/css/common.css
@@ -554,6 +554,18 @@ code {
 	margin-left: 0;
 }
 
+.update-php .update-maintenance-start {
+	margin-top: 30px;
+}
+.update-php .update-maintenance-content {
+	border-left: 2px dashed #aaa;
+	padding: 1px 0 20px 20px;
+	margin: -10px 0;
+}
+.update-php .update-maintenance-end {
+	margin-bottom: 30px;
+}
+
 .no-js .widefat thead .check-column input,
 .no-js .widefat tfoot .check-column input {
 	display: none;
diff --git src/wp-admin/includes/class-bulk-plugin-upgrader-skin.php src/wp-admin/includes/class-bulk-plugin-upgrader-skin.php
index a568d6b107..9e8fe2f69b 100644
--- src/wp-admin/includes/class-bulk-plugin-upgrader-skin.php
+++ src/wp-admin/includes/class-bulk-plugin-upgrader-skin.php
@@ -22,24 +22,38 @@ class Bulk_Plugin_Upgrader_Skin extends Bulk_Upgrader_Skin {
 		parent::add_strings();
 		/* translators: 1: name of plugin being updated, 2: number of updating plugin, 3: total number of plugins being updated */
 		$this->upgrader->strings['skin_before_update_header'] = __( 'Updating Plugin %1$s (%2$d/%3$d)' );
+		$this->upgrader->strings['bulk_download']             = __( 'Downloading plugin installers.' );
 	}
 
 	/**
-	 * @param string $title
+	 * Outputs copy before workload output.
+	 *
+	 * @param string $title Title or string identifier to use.
+	 *
+	 * @return void
 	 */
 	public function before( $title = '' ) {
-		parent::before( $this->plugin_info['Title'] );
+		$title = ( $title === '' ) ? $this->plugin_info['Title'] : $title;
+		parent::before( $title );
 	}
 
 	/**
-	 * @param string $title
+	 * Outputs copy after workload output.
+	 *
+	 * @param string $title Title or string identifier to use.
+	 *
+	 * @return void
 	 */
 	public function after( $title = '' ) {
-		parent::after( $this->plugin_info['Title'] );
+		$title = ( $title === '' ) ? $this->plugin_info['Title'] : $title;
+		parent::after( $title );
 		$this->decrement_update_count( 'plugin' );
 	}
 
 	/**
+	 * Outputs copy after bulk actions are all done.
+	 *
+	 * @return void
 	 */
 	public function bulk_footer() {
 		parent::bulk_footer();
diff --git src/wp-admin/includes/class-bulk-theme-upgrader-skin.php src/wp-admin/includes/class-bulk-theme-upgrader-skin.php
index ce426e0154..3c4d69ffe6 100644
--- src/wp-admin/includes/class-bulk-theme-upgrader-skin.php
+++ src/wp-admin/includes/class-bulk-theme-upgrader-skin.php
@@ -16,30 +16,53 @@
  * @see Bulk_Upgrader_Skin
  */
 class Bulk_Theme_Upgrader_Skin extends Bulk_Upgrader_Skin {
-	public $theme_info = array(); // Theme_Upgrader::bulk_upgrade() will fill this in.
+	/**
+	 * Theme information, which will be filled by Theme_Upgrader::bulk_upgrade()
+	 * @var array
+	 */
+	public $theme_info = array();
 
+	/**
+	 * Registers strings that will be used.
+	 *
+	 * @return void
+	 */
 	public function add_strings() {
 		parent::add_strings();
 		/* translators: 1: name of theme being updated, 2: number of updating themes, 3: total number of themes being updated */
 		$this->upgrader->strings['skin_before_update_header'] = __( 'Updating Theme %1$s (%2$d/%3$d)' );
+		$this->upgrader->strings['bulk_download']             = __( 'Downloading theme installers.' );
 	}
 
 	/**
-	 * @param string $title
+	 * Outputs copy before workload output.
+	 *
+	 * @param string $title Title or string identifier to use.
+	 *
+	 * @return void
 	 */
 	public function before( $title = '' ) {
-		parent::before( $this->theme_info->display( 'Name' ) );
+		$title = ( $title === '' ) ? $this->theme_info->display( 'Name' ) : $title;
+		parent::before( $title );
 	}
 
 	/**
-	 * @param string $title
+	 * Outputs copy after workload output.
+	 *
+	 * @param string $title Title or string identifier to use.
+	 *
+	 * @return void
 	 */
 	public function after( $title = '' ) {
-		parent::after( $this->theme_info->display( 'Name' ) );
+		$title = ( $title === '' ) ? $this->theme_info->display( 'Name' ) : $title;
+		parent::after( $title );
 		$this->decrement_update_count( 'theme' );
 	}
 
 	/**
+	 * Outputs copy after bulk actions are all done.
+	 *
+	 * @return void
 	 */
 	public function bulk_footer() {
 		parent::bulk_footer();
diff --git src/wp-admin/includes/class-bulk-upgrade-element-base.php src/wp-admin/includes/class-bulk-upgrade-element-base.php
new file mode 100644
index 0000000000..b950d94c71
--- /dev/null
+++ src/wp-admin/includes/class-bulk-upgrade-element-base.php
@@ -0,0 +1,347 @@
+<?php
+
+/**
+ * Abstract base class for Bulk Upgrade Elements
+ *
+ * @since x.x
+ */
+abstract class WP_Bulk_Upgrade_Element_Base {
+	/**
+	 * Is this element already up-to-date
+	 *
+	 * @var bool
+	 */
+	protected $up_to_date = false;
+	/**
+	 * Has the package been downloaded
+	 *
+	 * @var bool
+	 */
+	protected $downloaded = false;
+	/**
+	 * Was the package unpacked properly
+	 *
+	 * @var bool
+	 */
+	protected $unpacked = false;
+	/**
+	 * Has the package been installed
+	 *
+	 * @var bool
+	 */
+	protected $installed = false;
+
+	/**
+	 * The verification status of a download
+
+	 * @var bool|WP_Error
+	 */
+	protected $download_verification_result = false;
+
+	/**
+	 * Identifier for this element (source)
+	 *
+	 * @var mixed
+	 */
+	protected $name;
+	/**
+	 * Upgrader to use
+	 *
+	 * @var WP_Upgrader
+	 */
+	protected $upgrader;
+
+	/**
+	 * Options of current element, used throughout the process
+	 *
+	 * @var array
+	 */
+	protected $options;
+	/**
+	 * Info about the current element, used by the upgrader
+	 *
+	 * @var mixed
+	 */
+	protected $info;
+	/**
+	 * Name of the download
+	 *
+	 * @var string
+	 */
+	protected $download;
+	/**
+	 * Working dir for the unpacked package
+	 *
+	 * @var string
+	 */
+	protected $working_dir;
+
+	/**
+	 * Final result of installation (via upgrader)
+	 *
+	 * @var mixed
+	 */
+	protected $result;
+
+	/**
+	 * WP_Bulk_Upgrader_Element constructor.
+	 *
+	 * @param WP_Upgrader $upgrader
+	 * @param mixed       $name
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function __construct( $name, WP_Upgrader $upgrader ) {
+		$this->name     = $name;
+		$this->upgrader = $upgrader;
+	}
+
+	/**
+	 * Retrievs the original identifier data.
+	 *
+	 * @return mixed
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function get_name() {
+		return $this->name;
+	}
+
+	/**
+	 * Retrieves the cummulated result of the upgrade.
+	 *
+	 * @return bool
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function get_result() {
+		return isset( $this->result ) ? $this->result : $this->installed;
+	}
+
+	/**
+	 * Retrieves the result index for the current result.
+	 *
+	 * @return bool|int
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function get_result_index() {
+		return false;
+	}
+
+	/**
+	 * Determines if it is active in the system.
+	 *
+	 * @return boolean
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	abstract public function is_active();
+
+	/**
+	 * Determines if it is downloaded correctly.
+	 *
+	 * @return bool
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function is_downloaded() {
+		return $this->downloaded;
+	}
+
+	/**
+	 * Determines if it is unpacked succesfully.
+	 *
+	 * @return bool
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function is_unpacked() {
+		return $this->unpacked;
+	}
+
+	/**
+	 * Determines if it is installed succesfully.
+	 *
+	 * @return bool
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function is_installed() {
+		return $this->installed;
+	}
+
+	/**
+	 * Determines if it is already up-to-date.
+	 *
+	 * @return bool
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function is_up_to_date() {
+		return $this->up_to_date;
+	}
+
+	/**
+	 * Retrieves element info.
+	 *
+	 * @return mixed
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function get_info() {
+		return $this->info;
+	}
+
+	/**
+	 * Applies defaults and filter to options.
+	 *
+	 * @param array $options Options to use.
+	 * @retun void
+	 *
+	 * @since x.x
+	 * @access protected
+	 */
+	protected function set_options( $options ) {
+		$defaults = array(
+			'package'                     => '',
+			// Please always pass this.
+			'destination'                 => '',
+			// And this
+			'clear_destination'           => false,
+			'abort_if_destination_exists' => true,
+			// Abort if the Destination directory exists, Pass clear_destination as false please
+			'clear_working'               => true,
+			'is_multi'                    => false,
+			'hook_extra'                  => array()
+			// Pass any extra $hook_extra args here, this will be passed to any hooked filters.
+		);
+
+		$options = wp_parse_args( $options, $defaults );
+
+		/** This filter is documented in wp-admin/includes/class-wp-upgrader.php */
+		$this->options = apply_filters( 'upgrader_package_options', $options );
+	}
+
+	/**
+	 * Download this element.
+	 *
+	 * @return bool|string|WP_Error
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function download() {
+		if ( $this->is_up_to_date() ) {
+			return false;
+		}
+
+		// Connect to the Filesystem first.
+		$result = $this->upgrader->fs_connect( array( WP_CONTENT_DIR, $this->options['destination'] ) );
+		// Mainly for non-connected filesystem.
+		if ( ! $result || is_wp_error( $result ) ) {
+			return $result;
+		}
+
+		$this->download_verification_result = true;
+
+		/*
+		 * Download the package (Note, This just returns the filename
+		 * of the file if the package is a local file)
+		 */
+		$result = $this->upgrader->download_package( $this->options['package'], true );
+
+		if ( ! is_wp_error( $result ) ) {
+			$this->downloaded = true;
+			$this->download   = $result;
+		} else {
+			$filename = $result->get_error_data( 'softfail-filename' );
+			if ( $filename ) {
+				$this->download_verification_result = $result;
+
+				$result = $filename;
+
+				$this->downloaded = true;
+				$this->download   = $result;
+			}
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Unpacks this element.
+	 *
+	 * @return bool|string|WP_Error
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function unpack() {
+		if ( ! $this->is_downloaded() ) {
+			return false;
+		}
+
+		// Do not delete a "local" file
+		$delete_package = ( $this->download !== $this->options['package'] );
+
+		// Unzips the file into a temporary directory.
+		$result = $this->upgrader->unpack_package( $this->download, $delete_package );
+		if ( ! is_wp_error( $result ) ) {
+			$this->unpacked    = true;
+			$this->working_dir = $result;
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Installs this element.
+	 *
+	 * @return array|bool|WP_Error
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function install() {
+		if ( ! $this->is_unpacked() ) {
+			return false;
+		}
+
+		// With the given options, this installs it to the destination directory.
+		$result = $this->upgrader->install_package( array(
+			'source'                      => $this->working_dir,
+			'destination'                 => $this->options['destination'],
+			'clear_destination'           => $this->options['clear_destination'],
+			'abort_if_destination_exists' => $this->options['abort_if_destination_exists'],
+			'clear_working'               => $this->options['clear_working'],
+			'hook_extra'                  => $this->options['hook_extra']
+		) );
+
+		$this->result = $this->upgrader->result;
+
+		return $result;
+	}
+
+	/**
+	 * Retrieves the verification result status.
+	 *
+	 * @return bool|WP_Error
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function get_verification_result() {
+		return $this->download_verification_result;
+	}
+}
diff --git src/wp-admin/includes/class-bulk-upgrade-repository-element-base.php src/wp-admin/includes/class-bulk-upgrade-repository-element-base.php
new file mode 100644
index 0000000000..7fe211574c
--- /dev/null
+++ src/wp-admin/includes/class-bulk-upgrade-repository-element-base.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Abstract base class for Bulk Upgrade Elements that are formatted as Plugin/Theme
+ *
+ * @since x.x
+ */
+abstract class WP_Bulk_Upgrade_Repository_Element_Base extends WP_Bulk_Upgrade_Element_Base {
+	/**
+	 * Cache for current element type information.
+	 *
+	 * @var object
+	 */
+	protected static $current;
+
+	/**
+	 * Current element response information from repository.
+	 *
+	 * @var object
+	 */
+	protected $response;
+
+	/**
+	 * Retrieves the result index for the current item.
+	 *
+	 * @return mixed
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function get_result_index() {
+		return $this->name;
+	}
+
+	/**
+	 * Retrieves the current item.
+	 *
+	 * @return mixed
+	 *
+	 * @since x.x
+	 * @access protected
+	 */
+	abstract protected function get_current();
+
+	/**
+	 * Retrieves plugin repository response.
+	 *
+	 * @return bool|object Response from the respository as stdClass
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function get_response() {
+		$current = $this->get_current();
+
+		if ( is_object( $current ) && is_array( $current->response ) ) {
+			return $this->response = isset( $current->response[ $this->name ] ) ? $current->response[ $this->name ] : false;
+		}
+
+		return false;
+	}
+}
diff --git src/wp-admin/includes/class-bulk-upgrader-base.php src/wp-admin/includes/class-bulk-upgrader-base.php
new file mode 100644
index 0000000000..266cec600f
--- /dev/null
+++ src/wp-admin/includes/class-bulk-upgrader-base.php
@@ -0,0 +1,489 @@
+<?php
+
+/**
+ * Abstract base class for Bulk Upgrades
+ *
+ * @since x.x
+ */
+abstract class WP_Bulk_Upgrader_Base {
+	/**
+	 * Upgrader to use.
+	 *
+	 * @var WP_Upgrader
+	 */
+	protected $upgrader;
+
+	/**
+	 * Elements to upgrade.
+	 *
+	 * @var WP_Bulk_Upgrade_Element_Base[]
+	 */
+	protected $elements = array();
+
+	/**
+	 * Current element offset used to show total progression.
+	 *
+	 * @var int
+	 */
+	protected $current_element_offset = 0;
+
+	/**
+	 * WP_Bulk_Upgrader_Composite constructor.
+	 *
+	 * @param WP_Upgrader $upgrader     Upgrader to report back to
+	 * @param array       $elements     List of elements to be bulked.
+	 * @param string      $elementClass Class name to wrap elements in. Extended of WP_Bulk_Upgrader_Element.
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function __construct( WP_Upgrader $upgrader, array $elements, $elementClass ) {
+		if ( ! is_string( $elementClass ) ) {
+			throw new InvalidArgumentException( 'Expected element class to be string, got ' . gettype( $elementClass ) );
+		}
+
+		if ( ! class_exists( $elementClass ) ) {
+			throw new InvalidArgumentException( sprintf( 'Element class "%s" does not exist.', $elementClass ) );
+		}
+
+		$this->upgrader = $upgrader;
+
+		if ( array() !== $elements ) {
+			foreach ( $elements as $element ) {
+				$this->elements[] = new $elementClass( $element, $upgrader );
+			}
+		}
+
+		$this->upgrader->update_count   = count( $this->elements );
+		$this->upgrader->update_current = 0;
+	}
+
+	/**
+	 * Runs the bulk upgrade.
+	 *
+	 * @return array List of results.
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function run() {
+		$this->upgrader->clean_upgrade_folder();
+
+		$this->check_up_to_date( $this->elements );
+
+		// Get all elements that have an upgrade.
+		$upgradable_elements = $this->get_upgradable_elements( $this->elements );
+
+		$this->upgrader->update_count = count( $upgradable_elements );
+
+		// Download and unpack all items with upgrades.
+		$this->prepare_upgrade( $upgradable_elements );
+
+		// On multisite it is hard to determine if a plugin is active on a sub-site or not - assume all are active.
+		if ( is_multisite() ) {
+			$this->upgrade( $upgradable_elements, true );
+		}
+
+		if ( ! is_multisite() ) {
+			// Get all non-active elements - these don't need maintenance mode enabled.
+			$inactive_elements = $this->get_inactive_elements( $upgradable_elements );
+			if ( $inactive_elements ) {
+				$this->upgrade( $inactive_elements, false );
+			}
+
+			// Get all active elements - these need maintenance mode enabled.
+			$active_elements = $this->get_active_elements( $upgradable_elements );
+			if ( $active_elements ) {
+				$this->upgrade( $active_elements, true );
+			}
+		}
+
+		$this->upgrader->update_current = false;
+
+		return $this->get_results();
+	}
+
+	/**
+	 * Prepare download and unpack before installing.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements List of elements to prepare.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function prepare_upgrade( $elements ) {
+		$skin = $this->get_skin();
+
+		$skin->before( 'bulk_download' );
+		$this->download( $elements );
+		$skin->set_result(true);
+		$skin->after( 'bulk_download' );
+
+		$skin->before( 'bulk_unpack' );
+		$this->unpack( $elements );
+		$skin->after( 'bulk_unpack' );
+	}
+
+	/**
+	 * Upgrades a list of elements.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements         Elements to upgrade.
+	 * @param boolean                        $maintenance_mode Enable maintenance mode.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function upgrade( $elements, $maintenance_mode ) {
+		if ( $maintenance_mode ) {
+			$this->upgrader->maintenance_mode( true );
+		}
+
+		$this->install( $elements );
+
+		if ( $maintenance_mode ) {
+			$this->upgrader->maintenance_mode( false );
+		}
+
+		$this->current_element_offset += count( $elements );
+	}
+
+	/**
+	 * Downloads all the elements.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function download( array $elements ) {
+		/** @var $element WP_Bulk_Upgrade_Element_Base */
+		foreach ( $elements as $element ) {
+			$this->set_current_element( $element, $elements );
+
+			$result = $element->download();
+			if ( is_wp_error( $result ) ) {
+				$this->show_error( $result, $element );
+				break;
+			}
+
+			if ( is_wp_error( $element->get_verification_result() ) ) {
+				$this->show_warning( $element->get_verification_result(), $element );
+			}
+		}
+	}
+
+	/**
+	 * Unpacks downloaded elements.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements Elements to unpack.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function unpack( array $elements ) {
+		/** @var $element WP_Bulk_Upgrade_Element_Base */
+		foreach ( $elements as $element ) {
+			$this->set_current_element( $element, $elements );
+
+			$result = $element->unpack();
+			if ( is_wp_error( $result ) ) {
+				$this->show_error( $result, $element );
+			}
+		}
+	}
+
+	/**
+	 * Installs elements.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements Elements to install.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function install( array $elements ) {
+		foreach ( $elements as $element ) {
+			$this->set_current_element( $element, $elements );
+
+			$skin = $this->get_skin();
+			$skin->before();
+
+			$result = $element->install();
+
+			$skin->set_result( $result );
+			if ( is_wp_error( $result ) ) {
+				$skin->error( $result );
+				$skin->feedback( 'process_failed' );
+			} else {
+				// Install succeeded.
+				$skin->feedback( 'process_success' );
+			}
+			$skin->after();
+
+			if ( is_wp_error( $result ) && $this->abort_on_error() ) {
+				break;
+			}
+		}
+
+		$this->clear_current_element();
+	}
+
+	/**
+	 * Retrieves the elements that have an upgrade available.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements List of elements to filter.
+	 *
+	 * @return array Filtered list of elements with an upgrade.
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function get_upgradable_elements( array $elements ) {
+		return array_values( array_filter( $elements, array( $this, 'is_upgradable' ) ) );
+	}
+
+	/**
+	 * Determines if an element has an update.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element Element to check.
+	 *
+	 * @return bool True if an upgrade is available, otherwise False.
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function is_upgradable( WP_Bulk_Upgrade_Element_Base $element ) {
+		return ! $element->is_up_to_date();
+	}
+
+	/**
+	 * Retrieves the elements that are not active.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements Elements to check.
+	 *
+	 * @return array Filtered list of elements that are not active.
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function get_inactive_elements( array $elements ) {
+		return array_values( array_filter( $elements, array( $this, 'is_inactive') ) );
+	}
+
+	/**
+	 * Determines if an element is active.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element Element to check.
+	 *
+	 * @return bool True if the element is not active, False otherwise.
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function is_inactive( WP_Bulk_Upgrade_Element_Base $element ) {
+		return ! $element->is_active();
+	}
+
+	/**
+	 * Retrieves a list of active elements.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements Elements to check.
+	 *
+	 * @return array Filtered list of elements that are active.
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function get_active_elements( array $elements ) {
+		return array_values( array_filter( $elements, array( $this, 'is_active' ) ) );
+	}
+
+	/**
+	 * Determines if an element is active.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element Element to check.
+	 *
+	 * @return bool True if the element is active, False otherwise.
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function is_active( WP_Bulk_Upgrade_Element_Base $element ) {
+		return $element->is_active();
+	}
+
+	/**
+	 * Set the current element as active on the upgrader
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element  Element to set index of.
+	 * @param array                        $elements List to search in.
+	 *
+	 * @return int Current index.
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function set_current_element( $element, $elements ) {
+		$index = array_search( $element, $elements, true );
+
+		if ( false === $index ) {
+			$this->upgrader->update_current = false;
+		}
+
+		if ( false !== $index ) {
+			$this->upgrader->update_current = $index + 1 + $this->current_element_offset;
+		}
+
+		return $this->upgrader->update_current;
+	}
+
+	/**
+	 * Removes the current element.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function clear_current_element() {
+		$this->upgrader->update_current = false;
+	}
+
+	/**
+	 * Checks if any of the elements is already up to date.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements Elements to check.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function check_up_to_date( array $elements ) {
+		/** @var $element WP_Bulk_Upgrade_Element_Base */
+		foreach ( $elements as $element ) {
+			if ( $element->is_up_to_date() ) {
+				$this->set_current_element( $element, $elements );
+
+				$this->handle_up_to_date( $element );
+
+				$skin = $this->get_skin();
+				$skin->feedback( 'up_to_date' );
+				$skin->after();
+			}
+		}
+	}
+
+	/**
+	 * Handles what to do when an element is already up to date.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element Relevant element.
+	 *
+	 * @return void
+	 *
+	 * @since x.x
+	 * @access protected
+	 */
+	abstract protected function handle_up_to_date( WP_Bulk_Upgrade_Element_Base $element );
+
+	/**
+	 * Retrieves the results of the upgrade process.
+	 *
+	 * @return array List of results.
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function get_results() {
+		$results = array();
+
+		/** @var $element WP_Bulk_Upgrade_Element_Base */
+		foreach ( $this->elements as $element ) {
+			$index = $element->get_result_index();
+			if ( false === $index ) {
+				$results[] = $element->get_result();
+			} else {
+				$results[ $index ] = $element->get_result();
+			}
+		}
+
+		return $results;
+	}
+
+	/**
+	 * Passes an error to the skin.
+	 *
+	 * @param string                       $error
+	 * @param WP_Bulk_Upgrade_Element_Base $element
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function show_error( $error, WP_Bulk_Upgrade_Element_Base $element ) {
+		$skin = $this->get_skin();
+		if ( $skin ) {
+			$skin->error( $error );
+			$skin->after();
+		}
+	}
+
+	/**
+	 * Passes a warning to the skin.
+	 *
+	 * @param WP_Error                     $warning Warning to show.
+	 * @param WP_Bulk_Upgrade_Element_Base $element Relevant element.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function show_warning( WP_Error $warning, WP_Bulk_Upgrade_Element_Base $element ) {
+		$skin = $this->get_skin();
+		if ( $skin ) {
+			foreach ( $warning->get_error_messages() as $message ) {
+				if ( $warning->get_error_data() && is_string( $warning->get_error_data() ) ) {
+					$skin->feedback( $message . ' ' . esc_html( strip_tags( $warning->get_error_data() ) ) );
+				} else {
+					$skin->feedback( $message );
+				}
+			}
+		}
+	}
+
+	/**
+	 * Determines if we should abort when encountering an error.
+	 *
+	 * @return bool
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function abort_on_error() {
+		// Prevent credentials auth screen from displaying multiple times.
+		return true;
+	}
+
+	/**
+	 * Shorthand retrieval of upgrader skin.
+	 *
+	 * @return WP_Upgrader_Skin
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function get_skin() {
+		return $this->upgrader->get_skin();
+	}
+}
diff --git src/wp-admin/includes/class-bulk-upgrader-skin.php src/wp-admin/includes/class-bulk-upgrader-skin.php
index c1454e62f1..16028a5f34 100644
--- src/wp-admin/includes/class-bulk-upgrader-skin.php
+++ src/wp-admin/includes/class-bulk-upgrader-skin.php
@@ -16,14 +16,22 @@
  * @see WP_Upgrader_Skin
  */
 class Bulk_Upgrader_Skin extends WP_Upgrader_Skin {
+	/**
+	 * Are we in an install/upgrade loop.
+	 *
+	 * @var bool
+	 */
 	public $in_loop = false;
+
 	/**
+	 * Did we encounter an error while installing/upgrading.
+	 *
 	 * @var string|false
 	 */
 	public $error = false;
 
 	/**
-	 * @param array $args
+	 * @param array $args Arguments used for this skin.
 	 */
 	public function __construct( $args = array() ) {
 		$defaults = array(
@@ -36,6 +44,9 @@ class Bulk_Upgrader_Skin extends WP_Upgrader_Skin {
 	}
 
 	/**
+	 * Register the strings that we need.
+	 *
+	 * @return void
 	 */
 	public function add_strings() {
 		$this->upgrader->strings['skin_upgrade_start'] = __( 'The update process is starting. This process may take a while on some hosts, so please be patient.' );
@@ -46,12 +57,20 @@ class Bulk_Upgrader_Skin extends WP_Upgrader_Skin {
 		/* translators: %s: Title of an update */
 		$this->upgrader->strings['skin_update_successful'] = __( '%s updated successfully.' );
 		$this->upgrader->strings['skin_upgrade_end']       = __( 'All updates have been completed.' );
+		$this->upgrader->strings['bulk_download']          = __( 'Downloading items.' );
+		$this->upgrader->strings['bulk_unpack']            = __( 'Unpacking downloaded installers.' );
 	}
 
 	/**
-	 * @param string $string
+	 * Outputs feedback.
+	 *
+	 * @param string $string Text to show.
+	 *
+	 * @return void
 	 */
 	public function feedback( $string ) {
+		$original_string = $string;
+
 		if ( isset( $this->upgrader->strings[ $string ] ) ) {
 			$string = $this->upgrader->strings[ $string ];
 		}
@@ -65,30 +84,50 @@ class Bulk_Upgrader_Skin extends WP_Upgrader_Skin {
 				$string = vsprintf( $string, $args );
 			}
 		}
+
 		if ( empty( $string ) ) {
 			return;
 		}
-		if ( $this->in_loop ) {
-			echo "$string<br />\n";
+
+		if ( $original_string === 'maintenance_start' ) {
+			printf( '<p class="update-maintenance-start"><strong>%s</strong></p><div class="update-maintenance-content">', $string );
+		} elseif( $original_string === 'maintenance_end' ) {
+			printf( '</div><p class="update-maintenance-end"><strong>%s</strong></p>', $string );
 		} else {
-			echo "<p>$string</p>\n";
+			if ( $this->in_loop ) {
+				$string = "$string<br/>";
+			} else {
+				$string = "<p>$string</p>";
+			}
+
+			echo "$string\n";
 		}
+
+		$this->flush_output();
 	}
 
 	/**
+	 * Outputs nothing, this will be displayed within a iframe.
+	 *
+	 * @return void
 	 */
 	public function header() {
 		// Nothing, This will be displayed within a iframe.
 	}
 
 	/**
+	 * Outputs nothing, this will be displayed within a iframe.
+	 *
+	 * @return void
 	 */
 	public function footer() {
 		// Nothing, This will be displayed within a iframe.
 	}
 
 	/**
-	 * @param string|WP_Error $error
+	 * Showns an error.
+	 *
+	 * @param string|WP_Error $error Error that will be shown.
 	 */
 	public function error( $error ) {
 		if ( is_string( $error ) && isset( $this->upgrader->strings[ $error ] ) ) {
@@ -110,47 +149,94 @@ class Bulk_Upgrader_Skin extends WP_Upgrader_Skin {
 	}
 
 	/**
+	 * Shows the bulk header copy.
+	 *
+	 * @return void
 	 */
 	public function bulk_header() {
 		$this->feedback( 'skin_upgrade_start' );
 	}
 
 	/**
+	 * Shows the bulk footer copy.
+	 *
+	 * @return void
 	 */
 	public function bulk_footer() {
 		$this->feedback( 'skin_upgrade_end' );
 	}
 
 	/**
-	 * @param string $title
+	 * Shows an header for an element.
+	 * side-effect: Flushes the output buffer
+	 *
+	 * @param string $title Copy or string identifier to use.
+	 *
+	 * @return void
 	 */
 	public function before( $title = '' ) {
 		$this->in_loop = true;
-		printf( '<h2>' . $this->upgrader->strings['skin_before_update_header'] . ' <span class="spinner waiting-' . $this->upgrader->update_current . '"></span></h2>', $title, $this->upgrader->update_current, $this->upgrader->update_count );
-		echo '<script type="text/javascript">jQuery(\'.waiting-' . esc_js( $this->upgrader->update_current ) . '\').css("display", "inline-block");</script>';
+
+		$format = $this->upgrader->strings['skin_before_update_header'];
+
+		$identifier = $this->upgrader->update_current;
+		if ( isset( $this->upgrader->strings[ $title ] ) ) {
+			$identifier = $title;
+			$format = $this->upgrader->strings[ $title ];
+		}
+
+		printf(
+			'<h2>' . $format . ' <span class="' . esc_attr( 'spinner waiting-' . $identifier ) . '"></span></h2>',
+			$title,
+			$this->upgrader->update_current,
+			$this->upgrader->update_count
+		);
+
+		echo '<script type="text/javascript">jQuery(\'.waiting-' . esc_js( $identifier ) . '\').css("display", "inline-block");</script>';
 		// This progress messages div gets moved via JavaScript when clicking on "Show details.".
-		echo '<div class="update-messages hide-if-js" id="progress-' . esc_attr( $this->upgrader->update_current ) . '"><p>';
+		echo '<div class="update-messages hide-if-js" id="' . esc_attr( 'progress-' . $identifier ) . '"><p>';
 		$this->flush_output();
 	}
 
 	/**
-	 * @param string $title
+	 * Shows a footer for an element.
+	 * side-effect: Flushes the output buffer
+	 *
+	 * @param string $title Copy or string identifier to use.
+	 *
+	 * @return void
 	 */
 	public function after( $title = '' ) {
+		$success_format = $this->upgrader->strings['skin_update_successful'];
+		$error_format = $this->upgrader->strings['skin_update_failed_error'];
+		$failed_format = $this->upgrader->strings['skin_update_failed'];
+
+		$identifier = $this->upgrader->update_current;
+		if ( isset( $this->upgrader->strings[ $title ] ) ) {
+			$identifier = $title;
+			$title = $this->upgrader->strings[ $title ];
+
+			$success_format = '';
+			$error_format = '%2$s';
+			$failed_format = '%s';
+		}
+
 		echo '</p></div>';
+
 		if ( $this->error || ! $this->result ) {
 			if ( $this->error ) {
-				echo '<div class="error"><p>' . sprintf( $this->upgrader->strings['skin_update_failed_error'], $title, '<strong>' . $this->error . '</strong>' ) . '</p></div>';
+				echo '<div class="error"><p>' . sprintf( $error_format, $title, '<strong>' . $this->error . '</strong>' ) . '</p></div>';
 			} else {
-				echo '<div class="error"><p>' . sprintf( $this->upgrader->strings['skin_update_failed'], $title ) . '</p></div>';
+				echo '<div class="error"><p>' . sprintf( $failed_format, $title ) . '</p></div>';
 			}
 
-			echo '<script type="text/javascript">jQuery(\'#progress-' . esc_js( $this->upgrader->update_current ) . '\').show();</script>';
+			echo '<script type="text/javascript">jQuery(\'#progress-' . esc_js( $identifier ) . '\').show();</script>';
 		}
+
 		if ( $this->result && ! is_wp_error( $this->result ) ) {
 			if ( ! $this->error ) {
-				echo '<div class="updated js-update-details" data-update-details="progress-' . esc_attr( $this->upgrader->update_current ) . '">' .
-					'<p>' . sprintf( $this->upgrader->strings['skin_update_successful'], $title ) .
+				echo '<div class="updated js-update-details" data-update-details="' . esc_attr( 'progress-' . $identifier ) . '">' .
+					'<p>' . sprintf( $success_format, $title ) .
 					' <button type="button" class="hide-if-no-js button-link js-update-details-toggle" aria-expanded="false">' . __( 'Show details.' ) . '</button>' .
 					'</p></div>';
 			}
@@ -163,6 +249,9 @@ class Bulk_Upgrader_Skin extends WP_Upgrader_Skin {
 	}
 
 	/**
+	 * Resets the state of this class.
+	 *
+	 * @return void
 	 */
 	public function reset() {
 		$this->in_loop = false;
@@ -170,6 +259,9 @@ class Bulk_Upgrader_Skin extends WP_Upgrader_Skin {
 	}
 
 	/**
+	 * Flushes the output buffer.
+	 *
+	 * @return void
 	 */
 	public function flush_output() {
 		wp_ob_end_flush_all();
diff --git src/wp-admin/includes/class-core-upgrader.php src/wp-admin/includes/class-core-upgrader.php
index 977f6ec9c6..e2c2ffdee2 100644
--- src/wp-admin/includes/class-core-upgrader.php
+++ src/wp-admin/includes/class-core-upgrader.php
@@ -32,6 +32,7 @@ class Core_Upgrader extends WP_Upgrader {
 		/* translators: %s: package URL */
 		$this->strings['downloading_package']   = sprintf( __( 'Downloading update from %s&#8230;' ), '<span class="code">%s</span>' );
 		$this->strings['unpack_package']        = __( 'Unpacking the update&#8230;' );
+		$this->strings['unpack_package_bulk']   = __( 'Unpacking the update&#8230; (%1$d/%2$d)' );
 		$this->strings['copy_failed']           = __( 'Could not copy files.' );
 		$this->strings['copy_failed_space']     = __( 'Could not copy files. You may have run out of disk space.' );
 		$this->strings['start_rollback']        = __( 'Attempting to roll back to previous version.' );
diff --git src/wp-admin/includes/class-plugin-bulk-upgrade-element.php src/wp-admin/includes/class-plugin-bulk-upgrade-element.php
new file mode 100644
index 0000000000..a73d5b549e
--- /dev/null
+++ src/wp-admin/includes/class-plugin-bulk-upgrade-element.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * Plugin implementation of the Bulk Upgrade Element
+ *
+ * @since x.x
+ */
+class WP_Plugin_Bulk_Upgrade_Element extends WP_Bulk_Upgrade_Repository_Element_Base {
+	/**
+	 * WP_Bulk_Upgrader_Plugin_Element constructor.
+	 *
+	 * @param string          $plugin   Current plugin
+	 * @param Plugin_Upgrader $upgrader Upgrader to use.
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function __construct( $plugin, Plugin_Upgrader $upgrader ) {
+		parent::__construct( $plugin, $upgrader );
+
+		$this->info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin, false, true );
+
+		$response = $this->get_response();
+		if ( false === $response ) {
+			$this->up_to_date = true;
+
+			return;
+		}
+
+		$options = array(
+			'package'           => $response->package,
+			'destination'       => WP_PLUGIN_DIR,
+			'clear_destination' => true,
+			'clear_working'     => true,
+			'is_multi'          => true,
+			'hook_extra'        => array(
+				'plugin' => $plugin
+			)
+		);
+
+		$this->set_options( $options );
+	}
+
+	/**
+	 * Determines if this plugin is activated.
+	 *
+	 * @return bool
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function is_active() {
+		return is_plugin_active( $this->name );
+	}
+
+	/**
+	 * Retrieves information for the plugin from the `update_plugins` transient.
+	 *
+	 * @return mixed
+	 *
+	 * @since x.x
+	 * @access protected
+	 */
+	protected function get_current() {
+		if ( ! isset( self::$current ) ) {
+			self::$current = get_site_transient( 'update_plugins' );
+		}
+
+		return self::$current;
+	}
+}
diff --git src/wp-admin/includes/class-plugin-bulk-upgrader.php src/wp-admin/includes/class-plugin-bulk-upgrader.php
new file mode 100644
index 0000000000..c49acf0650
--- /dev/null
+++ src/wp-admin/includes/class-plugin-bulk-upgrader.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * Plugin Bulk Upgrader implementation
+ *
+ * @since x.x
+ */
+class WP_Plugin_Bulk_Upgrader extends WP_Bulk_Upgrader_Base {
+	/**
+	 * WP_Plugin_Bulk_Upgrader constructor.
+	 *
+	 * @param Plugin_Upgrader $upgrader Upgrader to use.
+	 * @param array           $elements Elements to use.
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function __construct( Plugin_Upgrader $upgrader, array $elements = array() ) {
+		parent::__construct( $upgrader, $elements, 'WP_Plugin_Bulk_Upgrade_Element' );
+	}
+
+	/**
+	 * Communicates result to skin.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element Relevant element.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function handle_up_to_date( WP_Bulk_Upgrade_Element_Base $element ) {
+		$skin = $this->get_skin();
+		if ( $skin instanceof WP_Upgrader_Skin ) {
+			$skin->set_result( 'up_to_date' );
+		}
+	}
+
+	/**
+	 * Communicate plugin info to skin
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element  Element to set as current.
+	 * @param array                        $elements List to search in.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function set_current_element( $element, $elements ) {
+		parent::set_current_element( $element, $elements );
+
+		if ( ! $this->upgrader->update_current ) {
+			return;
+		}
+
+		$skin = $this->get_skin();
+		$skin->plugin_info = $element->get_info();
+	}
+}
diff --git src/wp-admin/includes/class-plugin-upgrader.php src/wp-admin/includes/class-plugin-upgrader.php
index c80c9c4c97..5811cd0f49 100644
--- src/wp-admin/includes/class-plugin-upgrader.php
+++ src/wp-admin/includes/class-plugin-upgrader.php
@@ -49,6 +49,7 @@ class Plugin_Upgrader extends WP_Upgrader {
 		/* translators: %s: package URL */
 		$this->strings['downloading_package']  = sprintf( __( 'Downloading update from %s&#8230;' ), '<span class="code">%s</span>' );
 		$this->strings['unpack_package']       = __( 'Unpacking the update&#8230;' );
+		$this->strings['unpack_package_bulk']  = __( 'Unpacking the update&#8230; (%1$s/%2$s)' );
 		$this->strings['remove_old']           = __( 'Removing the old version of the plugin&#8230;' );
 		$this->strings['remove_old_failed']    = __( 'Could not remove the old plugin.' );
 		$this->strings['process_failed']       = __( 'Plugin update failed.' );
@@ -66,6 +67,7 @@ class Plugin_Upgrader extends WP_Upgrader {
 		/* translators: %s: package URL */
 		$this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s&#8230;' ), '<span class="code">%s</span>' );
 		$this->strings['unpack_package']      = __( 'Unpacking the package&#8230;' );
+		$this->strings['unpack_package_bulk'] = __( 'Unpacking the update&#8230; (%1$d/%2$d)' );
 		$this->strings['installing_package']  = __( 'Installing the plugin&#8230;' );
 		$this->strings['no_files']            = __( 'The plugin contains no files.' );
 		$this->strings['process_failed']      = __( 'Plugin installation failed.' );
@@ -228,8 +230,6 @@ class Plugin_Upgrader extends WP_Upgrader {
 		$this->bulk = true;
 		$this->upgrade_strings();
 
-		$current = get_site_transient( 'update_plugins' );
-
 		add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ), 10, 4 );
 
 		$this->skin->header();
@@ -243,64 +243,8 @@ class Plugin_Upgrader extends WP_Upgrader {
 
 		$this->skin->bulk_header();
 
-		/*
-		 * Only start maintenance mode if:
-		 * - running Multisite and there are one or more plugins specified, OR
-		 * - a plugin with an update available is currently active.
-		 * @TODO: For multisite, maintenance mode should only kick in for individual sites if at all possible.
-		 */
-		$maintenance = ( is_multisite() && ! empty( $plugins ) );
-		foreach ( $plugins as $plugin ) {
-			$maintenance = $maintenance || ( is_plugin_active( $plugin ) && isset( $current->response[ $plugin ] ) );
-		}
-		if ( $maintenance ) {
-			$this->maintenance_mode( true );
-		}
-
-		$results = array();
-
-		$this->update_count   = count( $plugins );
-		$this->update_current = 0;
-		foreach ( $plugins as $plugin ) {
-			$this->update_current++;
-			$this->skin->plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin, false, true );
-
-			if ( ! isset( $current->response[ $plugin ] ) ) {
-				$this->skin->set_result( 'up_to_date' );
-				$this->skin->before();
-				$this->skin->feedback( 'up_to_date' );
-				$this->skin->after();
-				$results[ $plugin ] = true;
-				continue;
-			}
-
-			// Get the URL to the zip file.
-			$r = $current->response[ $plugin ];
-
-			$this->skin->plugin_active = is_plugin_active( $plugin );
-
-			$result = $this->run(
-				array(
-					'package'           => $r->package,
-					'destination'       => WP_PLUGIN_DIR,
-					'clear_destination' => true,
-					'clear_working'     => true,
-					'is_multi'          => true,
-					'hook_extra'        => array(
-						'plugin' => $plugin,
-					),
-				)
-			);
-
-			$results[ $plugin ] = $this->result;
-
-			// Prevent credentials auth screen from displaying multiple times
-			if ( false === $result ) {
-				break;
-			}
-		} //end foreach $plugins
-
-		$this->maintenance_mode( false );
+		$plugin_bulk_upgrader = new WP_Plugin_Bulk_Upgrader( $this, $plugins );
+		$results = $plugin_bulk_upgrader->run();
 
 		// Force refresh of plugin update information.
 		wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
diff --git src/wp-admin/includes/class-theme-bulk-upgrade-element.php src/wp-admin/includes/class-theme-bulk-upgrade-element.php
new file mode 100644
index 0000000000..05cab0e440
--- /dev/null
+++ src/wp-admin/includes/class-theme-bulk-upgrade-element.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * Theme implementation of the Bulk Upgrade Element
+ *
+ * @since x.x
+ */
+class WP_Theme_Bulk_Upgrade_Element extends WP_Bulk_Upgrade_Repository_Element_Base {
+	/**
+	 * WP_Bulk_Upgrader_Theme_Element constructor.
+	 *
+	 * @param string         $theme    Theme to use.
+	 * @param Theme_Upgrader $upgrader Upgrader to use.
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function __construct( $theme, Theme_Upgrader $upgrader ) {
+		parent::__construct( $theme, $upgrader );
+
+		$this->info = $upgrader->theme_info( $theme );
+
+		$response = $this->get_response();
+		if ( false === $response ) {
+			$this->up_to_date = true;
+
+			return;
+		}
+
+		$options = array(
+			'package'           => $this->response['package'],
+			'destination'       => get_theme_root( $theme ),
+			'clear_destination' => true,
+			'clear_working'     => true,
+			'is_multi'          => true,
+			'hook_extra'        => array(
+				'theme' => $theme
+			)
+		);
+
+		$this->set_options( $options );
+	}
+
+	/**
+	 * Determines if this theme is active.
+	 *
+	 * @return bool
+	 *
+	 * @since x.x
+	 * @access public
+	 */
+	public function is_active() {
+		return $this->name === get_stylesheet() || $this->name === get_template();
+	}
+
+	/**
+	 * Retrieves information about the theme from the `update_themes` transient.
+	 *
+	 * @return mixed
+	 *
+	 * @since x.x
+	 * @access protected
+	 */
+	protected function get_current() {
+		if ( ! isset( self::$current ) ) {
+			self::$current = get_site_transient( 'update_themes' );
+		}
+
+		return self::$current;
+	}
+}
diff --git src/wp-admin/includes/class-theme-bulk-upgrader.php src/wp-admin/includes/class-theme-bulk-upgrader.php
new file mode 100644
index 0000000000..de4c95327d
--- /dev/null
+++ src/wp-admin/includes/class-theme-bulk-upgrader.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Theme Bulk Upgrader implementation
+ *
+ * @since x.x
+ */
+class WP_Theme_Bulk_Upgrader extends WP_Bulk_Upgrader_Base {
+	/**
+	 * WP_Theme_Bulk_Upgrader constructor.
+	 *
+	 * @param Theme_Upgrader                 $upgrader Upgrader to use.
+	 * @param WP_Bulk_Upgrade_Element_Base[] $elements Elements to use.
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function __construct( Theme_Upgrader $upgrader, array $elements = array() ) {
+		parent::__construct( $upgrader, $elements, 'WP_Theme_Bulk_Upgrade_Element' );
+	}
+
+	/**
+	 * Handles what to do for an up-to-date theme.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element Element to use.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function handle_up_to_date( WP_Bulk_Upgrade_Element_Base $element ) {
+		$skin = $this->get_skin();
+		if ( $skin instanceof WP_Upgrader_Skin ) {
+			$skin->set_result( true );
+		}
+	}
+
+	/**
+	 * Sets the current element on the skin.
+	 *
+	 * @param WP_Bulk_Upgrade_Element_Base $element  Element to set as current.
+	 * @param array                        $elements List to search in.
+	 *
+	 * @return void
+	 *
+	 * @since  x.x
+	 * @access protected
+	 */
+	protected function set_current_element( $element, $elements ) {
+		parent::set_current_element( $element, $elements );
+
+		if ( ! $this->upgrader->update_current ) {
+			return;
+		}
+
+		$skin = $this->get_skin();
+		if ( $skin instanceof Bulk_Theme_Upgrader_Skin ) {
+			$skin->theme_info = $element->get_info();
+		}
+	}
+}
diff --git src/wp-admin/includes/class-theme-upgrader.php src/wp-admin/includes/class-theme-upgrader.php
index 7b521063f8..497717de97 100644
--- src/wp-admin/includes/class-theme-upgrader.php
+++ src/wp-admin/includes/class-theme-upgrader.php
@@ -48,6 +48,7 @@ class Theme_Upgrader extends WP_Upgrader {
 		/* translators: %s: package URL */
 		$this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s&#8230;' ), '<span class="code">%s</span>' );
 		$this->strings['unpack_package']      = __( 'Unpacking the update&#8230;' );
+		$this->strings['unpack_package_bulk'] = __( 'Unpacking the update&#8230; (%1$d/%2$d)' );
 		$this->strings['remove_old']          = __( 'Removing the old version of the theme&#8230;' );
 		$this->strings['remove_old_failed']   = __( 'Could not remove the old theme.' );
 		$this->strings['process_failed']      = __( 'Theme update failed.' );
@@ -64,6 +65,7 @@ class Theme_Upgrader extends WP_Upgrader {
 		/* translators: %s: package URL */
 		$this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s&#8230;' ), '<span class="code">%s</span>' );
 		$this->strings['unpack_package']      = __( 'Unpacking the package&#8230;' );
+		$this->strings['unpack_package_bulk'] = __( 'Unpacking the update&#8230; (%1$d/%2$d)' );
 		$this->strings['installing_package']  = __( 'Installing the theme&#8230;' );
 		$this->strings['no_files']            = __( 'The theme contains no files.' );
 		$this->strings['process_failed']      = __( 'Theme installation failed.' );
@@ -358,61 +360,8 @@ class Theme_Upgrader extends WP_Upgrader {
 
 		$this->skin->bulk_header();
 
-		// Only start maintenance mode if:
-		// - running Multisite and there are one or more themes specified, OR
-		// - a theme with an update available is currently in use.
-		// @TODO: For multisite, maintenance mode should only kick in for individual sites if at all possible.
-		$maintenance = ( is_multisite() && ! empty( $themes ) );
-		foreach ( $themes as $theme ) {
-			$maintenance = $maintenance || $theme == get_stylesheet() || $theme == get_template();
-		}
-		if ( $maintenance ) {
-			$this->maintenance_mode( true );
-		}
-
-		$results = array();
-
-		$this->update_count   = count( $themes );
-		$this->update_current = 0;
-		foreach ( $themes as $theme ) {
-			$this->update_current++;
-
-			$this->skin->theme_info = $this->theme_info( $theme );
-
-			if ( ! isset( $current->response[ $theme ] ) ) {
-				$this->skin->set_result( true );
-				$this->skin->before();
-				$this->skin->feedback( 'up_to_date' );
-				$this->skin->after();
-				$results[ $theme ] = true;
-				continue;
-			}
-
-			// Get the URL to the zip file
-			$r = $current->response[ $theme ];
-
-			$result = $this->run(
-				array(
-					'package'           => $r['package'],
-					'destination'       => get_theme_root( $theme ),
-					'clear_destination' => true,
-					'clear_working'     => true,
-					'is_multi'          => true,
-					'hook_extra'        => array(
-						'theme' => $theme,
-					),
-				)
-			);
-
-			$results[ $theme ] = $this->result;
-
-			// Prevent credentials auth screen from displaying multiple times
-			if ( false === $result ) {
-				break;
-			}
-		} //end foreach $plugins
-
-		$this->maintenance_mode( false );
+		$theme_bulk_upgrader = new WP_Theme_Bulk_Upgrader( $this, $themes );
+		$results = $theme_bulk_upgrader->run();
 
 		// Refresh the Theme Update information
 		wp_clean_themes_cache( $parsed_args['clear_update_cache'] );
diff --git src/wp-admin/includes/class-wp-upgrader-bulk.php src/wp-admin/includes/class-wp-upgrader-bulk.php
new file mode 100644
index 0000000000..6e1c43828c
--- /dev/null
+++ src/wp-admin/includes/class-wp-upgrader-bulk.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * The Bulk handlers for the WordPress Upgrader
+ *
+ * @package    WordPress
+ * @subpackage Upgrader
+ * @since      x.x
+ */
+
+require_once 'class-bulk-upgrade-element-base.php';
+require_once 'class-bulk-upgrade-repository-element-base.php';
+
+require_once 'class-bulk-upgrader-base.php';
+
+require_once 'class-plugin-bulk-upgrade-element.php';
+require_once 'class-plugin-bulk-upgrader.php';
+
+require_once 'class-theme-bulk-upgrade-element.php';
+require_once 'class-theme-bulk-upgrader.php';
diff --git src/wp-admin/includes/class-wp-upgrader-skin.php src/wp-admin/includes/class-wp-upgrader-skin.php
index ddcfd19f18..5b110fb0ff 100644
--- src/wp-admin/includes/class-wp-upgrader-skin.php
+++ src/wp-admin/includes/class-wp-upgrader-skin.php
@@ -15,6 +15,11 @@
  */
 class WP_Upgrader_Skin {
 
+	/**
+	 * Holds the Upgrader to use.
+	 *
+	 * @var WP_Upgrader
+	 */
 	public $upgrader;
 	public $done_header = false;
 	public $done_footer = false;
@@ -42,7 +47,9 @@ class WP_Upgrader_Skin {
 	}
 
 	/**
-	 * @param WP_Upgrader $upgrader
+	 * Sets the upgrader to use internally.
+	 *
+	 * @param WP_Upgrader $upgrader Upgrader to use.
 	 */
 	public function set_upgrader( &$upgrader ) {
 		if ( is_object( $upgrader ) ) {
diff --git src/wp-admin/includes/class-wp-upgrader.php src/wp-admin/includes/class-wp-upgrader.php
index b8258d4bc2..1737f46849 100644
--- src/wp-admin/includes/class-wp-upgrader.php
+++ src/wp-admin/includes/class-wp-upgrader.php
@@ -9,6 +9,9 @@
  * @since 2.8.0
  */
 
+/** WP_Upgrader_Bulk class */
+require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-bulk.php';
+
 /** WP_Upgrader_Skin class */
 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';
 
@@ -64,7 +67,7 @@ class WP_Upgrader {
 	 * @since 2.8.0
 	 * @var Automatic_Upgrader_Skin|WP_Upgrader_Skin $skin
 	 */
-	public $skin = null;
+	public $skin;
 
 	/**
 	 * The result of the installation.
@@ -120,7 +123,7 @@ class WP_Upgrader {
 	 *                               instance.
 	 */
 	public function __construct( $skin = null ) {
-		if ( null == $skin ) {
+		if ( $skin === null ) {
 			$this->skin = new WP_Upgrader_Skin();
 		} else {
 			$this->skin = $skin;
@@ -300,18 +303,14 @@ class WP_Upgrader {
 	public function unpack_package( $package, $delete_package = true ) {
 		global $wp_filesystem;
 
-		$this->skin->feedback( 'unpack_package' );
+		if ( $this->update_count > 1 ) {
+			$this->skin->feedback( 'unpack_package_bulk', $this->update_current, $this->update_count );
+		} else {
+			$this->skin->feedback( 'unpack_package' );
+		}
 
 		$upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
 
-		//Clean up contents of upgrade directory beforehand.
-		$upgrade_files = $wp_filesystem->dirlist( $upgrade_folder );
-		if ( ! empty( $upgrade_files ) ) {
-			foreach ( $upgrade_files as $file ) {
-				$wp_filesystem->delete( $upgrade_folder . $file['name'], true );
-			}
-		}
-
 		// We need a working directory - Strip off any .tmp or .zip suffixes
 		$working_dir = $upgrade_folder . basename( basename( $package, '.tmp' ), '.zip' );
 
@@ -702,9 +701,7 @@ class WP_Upgrader {
 		 */
 		$options = apply_filters( 'upgrader_package_options', $options );
 
-		if ( ! $options['is_multi'] ) { // call $this->header separately if running multiple times
-			$this->skin->header();
-		}
+		$this->skin->header();
 
 		// Connect to the Filesystem first.
 		$res = $this->fs_connect( array( WP_CONTENT_DIR, $options['destination'] ) );
@@ -727,6 +724,8 @@ class WP_Upgrader {
 			return $res;
 		}
 
+		$this->clean_upgrade_folder();
+
 		/*
 		 * Download the package (Note, This just returns the filename
 		 * of the file if the package is a local file)
@@ -925,6 +924,39 @@ class WP_Upgrader {
 		return delete_option( $lock_name . '.lock' );
 	}
 
+	/**
+	 * Clears optional remnants of previous upgrades
+	 *
+	 * @since x.x
+	 * @access public
+	 *
+	 * @global WP_Filesystem_Base $wp_filesystem Subclass
+	 */
+	public function clean_upgrade_folder() {
+		/** @var WP_Filesystem_Base $wp_filesystem */
+		global $wp_filesystem;
+
+		//Clean up contents of upgrade directory.
+		$upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
+		$upgrade_files  = $wp_filesystem->dirlist( $upgrade_folder );
+		if ( ! empty( $upgrade_files ) ) {
+			foreach ( $upgrade_files as $file ) {
+				$wp_filesystem->delete( $upgrade_folder . $file['name'], true );
+			}
+		}
+	}
+
+	/**
+	 * Retrieves the used skin.
+	 *
+	 * @return WP_Upgrader_Skin The used skin.
+	 *
+	 * @since  x.x
+	 * @access public
+	 */
+	public function get_skin() {
+		return $this->skin;
+	}
 }
 
 /** Plugin_Upgrader class */
