diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
index 0a61a86..d7c2e75 100644
--- src/wp-admin/js/customize-controls.js
+++ src/wp-admin/js/customize-controls.js
@@ -3786,6 +3786,21 @@
 			});
 		});
 
+		// Focus on the control that is associated with the given setting.
+		api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
+			var matchedControl;
+			api.control.each( function( control ) {
+				var settingIds = _.pluck( control.settings, 'id' );
+				if ( -1 !== _.indexOf( settingIds, settingId ) ) {
+					matchedControl = control;
+				}
+			} );
+
+			if ( matchedControl ) {
+				matchedControl.focus();
+			}
+		} );
+
 		api.trigger( 'ready' );
 
 		// Make sure left column gets focus
diff --git src/wp-admin/js/customize-nav-menus.js src/wp-admin/js/customize-nav-menus.js
index 6dea2b4..8e345e9 100644
--- src/wp-admin/js/customize-nav-menus.js
+++ src/wp-admin/js/customize-nav-menus.js
@@ -19,7 +19,7 @@
 	api.Menus.data = {
 		itemTypes: [],
 		l10n: {},
-		menuItemTransport: 'postMessage',
+		settingTransport: 'refresh',
 		phpIntMax: 0,
 		defaultSettingValues: {
 			nav_menu: {},
@@ -2307,7 +2307,7 @@
 			customizeId = 'nav_menu_item[' + String( placeholderId ) + ']';
 			settingArgs = {
 				type: 'nav_menu_item',
-				transport: 'postMessage',
+				transport: api.Menus.data.settingTransport,
 				previewer: api.previewer
 			};
 			setting = api.create( customizeId, customizeId, {}, settingArgs );
@@ -2396,7 +2396,7 @@
 			// Register the menu control setting.
 			api.create( customizeId, customizeId, {}, {
 				type: 'nav_menu',
-				transport: 'postMessage',
+				transport: api.Menus.data.settingTransport,
 				previewer: api.previewer
 			} );
 			api( customizeId ).set( $.extend(
@@ -2532,7 +2532,7 @@
 				newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
 				newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
 					type: 'nav_menu',
-					transport: 'postMessage',
+					transport: api.Menus.data.settingTransport,
 					previewer: api.previewer
 				} );
 
@@ -2680,7 +2680,7 @@
 				newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']';
 				newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
 					type: 'nav_menu_item',
-					transport: 'postMessage',
+					transport: api.Menus.data.settingTransport,
 					previewer: api.previewer
 				} );
 
diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js
index 360c183..91a6516 100644
--- src/wp-admin/js/customize-widgets.js
+++ src/wp-admin/js/customize-widgets.js
@@ -34,7 +34,7 @@
 		multi_number: null,
 		name: null,
 		id_base: null,
-		transport: 'refresh',
+		transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh',
 		params: [],
 		width: null,
 		height: null,
@@ -1982,7 +1982,7 @@
 			isExistingWidget = api.has( settingId );
 			if ( ! isExistingWidget ) {
 				settingArgs = {
-					transport: 'refresh',
+					transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh',
 					previewer: this.setting.previewer
 				};
 				setting = api.create( settingId, settingId, '', settingArgs );
diff --git src/wp-content/themes/twentythirteen/js/theme-customizer.js src/wp-content/themes/twentythirteen/js/theme-customizer.js
index 6072104..8519752 100644
--- src/wp-content/themes/twentythirteen/js/theme-customizer.js
+++ src/wp-content/themes/twentythirteen/js/theme-customizer.js
@@ -38,4 +38,16 @@
 			}
 		} );
 	} );
+
+	if ( wp.customize.selectiveRefresh ) {
+		wp.customize.selectiveRefresh.bind( 'sidebar-updated', function( sidebarPartial ) {
+			var widgetArea;
+			if ( 'sidebar-1' === sidebarPartial.sidebarId && $.isFunction( $.fn.masonry ) ) {
+				widgetArea = $( '#secondary .widget-area' );
+				widgetArea.masonry( 'destroy' );
+				widgetArea.masonry();
+			}
+		} );
+	}
+
 } )( jQuery );
diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
index 942d907..a58cb94 100644
--- src/wp-includes/class-wp-customize-manager.php
+++ src/wp-includes/class-wp-customize-manager.php
@@ -67,6 +67,15 @@ final class WP_Customize_Manager {
 	public $nav_menus;
 
 	/**
+	 * Methods and properties dealing with selective refresh in the Customizer preview.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @var WP_Customize_Selective_Refresh
+	 */
+	public $selective_refresh;
+
+	/**
 	 * Registered instances of WP_Customize_Setting.
 	 *
 	 * @since 3.4.0
@@ -100,7 +109,7 @@ final class WP_Customize_Manager {
 	 * @access protected
 	 * @var array
 	 */
-	protected $components = array( 'widgets', 'nav_menus' );
+	protected $components = array( 'widgets', 'nav_menus', 'selective_refresh' );
 
 	/**
 	 * Registered instances of WP_Customize_Section.
@@ -249,14 +258,18 @@ final class WP_Customize_Manager {
 		 */
 		$components = apply_filters( 'customize_loaded_components', $this->components, $this );
 
-		if ( in_array( 'widgets', $components ) ) {
+		if ( in_array( 'widgets', $components, true ) ) {
 			require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
 			$this->widgets = new WP_Customize_Widgets( $this );
 		}
-		if ( in_array( 'nav_menus', $components ) ) {
+		if ( in_array( 'nav_menus', $components, true ) ) {
 			require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
 			$this->nav_menus = new WP_Customize_Nav_Menus( $this );
 		}
+		if ( in_array( 'selective_refresh', $components, true ) ) {
+			require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
+			$this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
+		}
 
 		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
 
@@ -1711,6 +1724,7 @@ final class WP_Customize_Manager {
 			'autofocus' => array(),
 			'documentTitleTmpl' => $this->get_document_title_template(),
 			'previewableDevices' => $this->get_previewable_devices(),
+			'selectiveRefreshEnabled' => isset( $this->selective_refresh ),
 		);
 
 		// Prepare Customize Section objects to pass to JavaScript.
diff --git src/wp-includes/class-wp-customize-nav-menus.php src/wp-includes/class-wp-customize-nav-menus.php
index 5453c17..2b355bd 100644
--- src/wp-includes/class-wp-customize-nav-menus.php
+++ src/wp-includes/class-wp-customize-nav-menus.php
@@ -61,6 +61,8 @@ final class WP_Customize_Nav_Menus {
 		add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
 		add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
 		add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
+
+		add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
 	}
 
 	/**
@@ -375,7 +377,7 @@ final class WP_Customize_Nav_Menus {
 				'reorderLabelOn'    => esc_attr__( 'Reorder menu items' ),
 				'reorderLabelOff'   => esc_attr__( 'Close reorder mode' ),
 			),
-			'menuItemTransport'    => 'postMessage',
+			'settingTransport'     => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
 			'phpIntMax'            => PHP_INT_MAX,
 			'defaultSettingValues' => array(
 				'nav_menu'      => $temp_nav_menu_setting->default,
@@ -425,11 +427,13 @@ final class WP_Customize_Nav_Menus {
 	public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
 		if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
 			$setting_args = array(
-				'type' => WP_Customize_Nav_Menu_Setting::TYPE,
+				'type'      => WP_Customize_Nav_Menu_Setting::TYPE,
+				'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
 			);
 		} elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
 			$setting_args = array(
-				'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
+				'type'      => WP_Customize_Nav_Menu_Item_Setting::TYPE,
+				'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
 			);
 		}
 		return $setting_args;
@@ -514,7 +518,7 @@ final class WP_Customize_Nav_Menus {
 
 			$setting = $this->manager->get_setting( $setting_id );
 			if ( $setting ) {
-				$setting->transport = 'postMessage';
+				$setting->transport = isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh';
 				remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
 				add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
 			} else {
@@ -522,7 +526,7 @@ final class WP_Customize_Nav_Menus {
 					'sanitize_callback' => array( $this, 'intval_base10' ),
 					'theme_supports'    => 'menus',
 					'type'              => 'theme_mod',
-					'transport'         => 'postMessage',
+					'transport'         => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
 					'default'           => 0,
 				) );
 			}
@@ -548,7 +552,9 @@ final class WP_Customize_Nav_Menus {
 			) ) );
 
 			$nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
-			$this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
+			$this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id, array(
+				'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
+			) ) );
 
 			// Add the menu contents.
 			$menu_items = (array) wp_get_nav_menu_items( $menu_id );
@@ -561,7 +567,8 @@ final class WP_Customize_Nav_Menus {
 				$value = (array) $item;
 				$value['nav_menu_term_id'] = $menu_id;
 				$this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array(
-					'value' => $value,
+					'value'     => $value,
+					'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
 				) ) );
 
 				// Create a control for each menu item.
@@ -585,7 +592,7 @@ final class WP_Customize_Nav_Menus {
 		$this->manager->add_setting( 'new_menu_name', array(
 			'type'      => 'new_menu',
 			'default'   => '',
-			'transport' => 'postMessage',
+			'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
 		) );
 
 		$this->manager->add_control( 'new_menu_name', array(
@@ -801,28 +808,37 @@ final class WP_Customize_Nav_Menus {
 	<?php
 	}
 
-	// Start functionality specific to partial-refresh of menu changes in Customizer preview.
-	const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
-	const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
-	const RENDER_QUERY_VAR = 'wp_customize_menu_render';
-
-	/**
-	 * The number of wp_nav_menu() calls which have happened in the preview.
-	 *
-	 * @since 4.3.0
-	 * @access public
-	 * @var int
+	/*
+	 * Start functionality specific to partial-refresh of menu changes in Customizer preview.
 	 */
-	public $preview_nav_menu_instance_number = 0;
 
 	/**
-	 * Nav menu args used for each instance.
+	 * Filter args for dynamic nav_menu partials.
 	 *
-	 * @since 4.3.0
-	 * @access public
-	 * @var array
+	 * @since 4.5.0
+	 *
+	 * @param array|false $partial_args Partial args.
+	 * @param string      $partial_id  Partial ID.
+	 * @return array Partial args
 	 */
-	public $preview_nav_menu_instance_args = array();
+	public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
+
+		if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) {
+			if ( false === $partial_args ) {
+				$partial_args = array();
+			}
+			$partial_args = array_merge(
+				$partial_args,
+				array(
+					'type' => 'nav_menu_instance',
+					'render_callback' => array( $this, 'render_nav_menu_partial' ),
+					'container_inclusive' => true,
+				)
+			);
+		}
+
+		return $partial_args;
+	}
 
 	/**
 	 * Add hooks for the Customizer preview.
@@ -831,13 +847,9 @@ final class WP_Customize_Nav_Menus {
 	 * @access public
 	 */
 	public function customize_preview_init() {
-		add_action( 'template_redirect', array( $this, 'render_menu' ) );
 		add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
-
-		if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
-			add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
-			add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
-		}
+		add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
+		add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
 	}
 
 	/**
@@ -845,16 +857,13 @@ final class WP_Customize_Nav_Menus {
 	 *
 	 * @since 4.3.0
 	 * @access public
-	 *
 	 * @see wp_nav_menu()
+	 * @see WP_Customize_Widgets_Partial_Refresh::filter_dynamic_sidebar_params()
 	 *
 	 * @param array $args An array containing wp_nav_menu() arguments.
 	 * @return array Arguments.
 	 */
 	public function filter_wp_nav_menu_args( $args ) {
-		$this->preview_nav_menu_instance_number += 1;
-		$args['instance_number'] = $this->preview_nav_menu_instance_number;
-
 		$can_partial_refresh = (
 			! empty( $args['echo'] )
 			&&
@@ -867,30 +876,34 @@ final class WP_Customize_Nav_Menus {
 				||
 				( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
 			)
+			&&
+			(
+				! empty( $args['container'] )
+				||
+				( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
+			)
 		);
-		$args['can_partial_refresh'] = $can_partial_refresh;
-
-		$hashed_args = $args;
-
 		if ( ! $can_partial_refresh ) {
-			$hashed_args['fallback_cb'] = '';
-			$hashed_args['walker'] = '';
+			return $args;
 		}
 
+		$exported_args = $args;
+
 		// Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes.
-		if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) {
-			$hashed_args['menu'] = $hashed_args['menu']->term_id;
+		if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) {
+			$exported_args['menu'] = $exported_args['menu']->term_id;
 		}
 
-		ksort( $hashed_args );
-		$hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args );
+		ksort( $exported_args );
+		$exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
+
+		$args['customize_preview_nav_menus_args'] = $exported_args;
 
-		$this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;
 		return $args;
 	}
 
 	/**
-	 * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
+	 * Prepare wp_nav_menu() calls for partial refresh. Injects attributes into container element.
 	 *
 	 * @since 4.3.0
 	 * @access public
@@ -902,20 +915,19 @@ final class WP_Customize_Nav_Menus {
 	 * @return null
 	 */
 	public function filter_wp_nav_menu( $nav_menu_content, $args ) {
-		if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
-			$nav_menu_content = preg_replace(
-				'/(?<=class=")/',
-				sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ),
-				$nav_menu_content,
-				1 // Only update the class on the first element found, the menu container.
-			);
+		if ( ! empty( $args->customize_preview_nav_menus_args ) ) {
+			$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
+			$attributes .= ' data-customize-partial-type="nav_menu_instance"';
+			$attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
+			$nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $nav_menu_content, 1 );
 		}
 		return $nav_menu_content;
 	}
 
 	/**
-	 * Hash (hmac) the arguments with the nonce and secret auth key to ensure they
-	 * are not tampered with when submitted in the Ajax request.
+	 * Hash (hmac) the nav menu args to ensure they are not tampered with when submitted in the Ajax request.
+	 *
+	 * Note that the array is expected to be pre-sorted.
 	 *
 	 * @since 4.3.0
 	 * @access public
@@ -924,7 +936,7 @@ final class WP_Customize_Nav_Menus {
 	 * @return string
 	 */
 	public function hash_nav_menu_args( $args ) {
-		return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
+		return wp_hash( serialize( $args ) );
 	}
 
 	/**
@@ -934,32 +946,25 @@ final class WP_Customize_Nav_Menus {
 	 * @access public
 	 */
 	public function customize_preview_enqueue_deps() {
-		wp_enqueue_script( 'customize-preview-nav-menus' );
-		wp_enqueue_style( 'customize-preview' );
+		if ( isset( $this->manager->selective_refresh ) ) {
+			$script = wp_scripts()->registered['customize-preview-nav-menus'];
+			$script->deps[] = 'customize-selective-refresh';
+		}
 
-		add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
+		wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
+		wp_enqueue_style( 'customize-preview' );
 	}
 
 	/**
 	 * Export data from PHP to JS.
 	 *
+	 * @deprecated
 	 * @since 4.3.0
+	 * @since 4.5.0 Obsolete.
 	 * @access public
 	 */
 	public function export_preview_data() {
-
-		// Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
-		$exports = array(
-			'renderQueryVar'        => self::RENDER_QUERY_VAR,
-			'renderNonceValue'      => wp_create_nonce( self::RENDER_AJAX_ACTION ),
-			'renderNoncePostKey'    => self::RENDER_NONCE_POST_KEY,
-			'navMenuInstanceArgs'   => $this->preview_nav_menu_instance_args,
-			'l10n'                  => array(
-				'editNavMenuItemTooltip' => __( 'Shift-click to edit this menu item.' ),
-			),
-		);
-
-		printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
+		_deprecated_function( __METHOD__, '4.5.0' );
 	}
 
 	/**
@@ -969,49 +974,29 @@ final class WP_Customize_Nav_Menus {
 	 * @access public
 	 *
 	 * @see wp_nav_menu()
+	 *
+	 * @param WP_Customize_Partial $partial       Partial.
+	 * @param array                $nav_menu_args Nav menu args supplied as container context.
+	 * @return string|false
 	 */
-	public function render_menu() {
-		if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
-			return;
-		}
+	public function render_nav_menu_partial( $partial, $nav_menu_args ) {
+		unset( $partial );
 
-		$this->manager->remove_preview_signature();
-
-		if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
-			wp_send_json_error( 'missing_nonce_param' );
-		}
-
-		if ( ! is_customize_preview() ) {
-			wp_send_json_error( 'expected_customize_preview' );
+		if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
+			return false; // Error: missing_args_hmac.
 		}
+		$nav_menu_args_hmac = $nav_menu_args['args_hmac'];
+		unset( $nav_menu_args['args_hmac'] );
 
-		if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
-			wp_send_json_error( 'nonce_check_fail' );
+		ksort( $nav_menu_args );
+		if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) {
+			return false; // Error: args_hmac_mismatch.
 		}
 
-		if ( ! current_user_can( 'edit_theme_options' ) ) {
-			wp_send_json_error( 'unauthorized' );
-		}
-
-		if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
-			wp_send_json_error( 'missing_param' );
-		}
-
-		if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
-			wp_send_json_error( 'missing_param' );
-		}
-
-		$wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
-		if ( ! is_array( $wp_nav_menu_args ) ) {
-			wp_send_json_error( 'wp_nav_menu_args_not_array' );
-		}
-
-		$wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
-		if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) {
-			wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
-		}
+		ob_start();
+		wp_nav_menu( $nav_menu_args );
+		$content = ob_get_clean();
 
-		$wp_nav_menu_args['echo'] = false;
-		wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
+		return $content;
 	}
 }
diff --git src/wp-includes/class-wp-customize-widgets.php src/wp-includes/class-wp-customize-widgets.php
index 5a0e62b..cf465e6 100644
--- src/wp-includes/class-wp-customize-widgets.php
+++ src/wp-includes/class-wp-customize-widgets.php
@@ -100,6 +100,10 @@ final class WP_Customize_Widgets {
 		add_action( 'dynamic_sidebar',                         array( $this, 'tally_rendered_widgets' ) );
 		add_filter( 'is_active_sidebar',                       array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
 		add_filter( 'dynamic_sidebar_has_widgets',             array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
+
+		add_filter( 'customize_dynamic_partial_args',          array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
+		add_filter( 'widget_customizer_setting_args',          array( $this, 'filter_widget_customizer_setting_args' ), 10, 2 );
+		add_action( 'customize_preview_init',                  array( $this, 'selective_refresh_init' ) );
 	}
 
 	/**
@@ -682,6 +686,7 @@ final class WP_Customize_Widgets {
 				'widgetReorderNav' => $widget_reorder_nav_tpl,
 				'moveWidgetArea'   => $move_widget_area_tpl,
 			),
+			'selectiveRefresh'     => isset( $this->manager->selective_refresh ),
 		);
 
 		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
@@ -762,7 +767,7 @@ final class WP_Customize_Widgets {
 		$args = array(
 			'type'       => 'option',
 			'capability' => 'edit_theme_options',
-			'transport'  => 'refresh',
+			'transport'  => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
 			'default'    => array(),
 		);
 
@@ -884,7 +889,7 @@ final class WP_Customize_Widgets {
 				'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
 				'is_disabled'  => $is_disabled,
 				'id_base'      => $id_base,
-				'transport'    => 'refresh',
+				'transport'    => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
 				'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
 				'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
 				'is_wide'      => $this->is_wide_widget( $widget['id'] ),
@@ -1061,8 +1066,9 @@ final class WP_Customize_Widgets {
 			'registeredSidebars' => array_values( $wp_registered_sidebars ),
 			'registeredWidgets'  => $wp_registered_widgets,
 			'l10n'               => array(
-				'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
+				'widgetTooltip'  => __( 'Shift-click to edit this widget.' ),
 			),
+			'selectiveRefresh'   => isset( $this->manager->selective_refresh ),
 		);
 		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
 			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
@@ -1459,9 +1465,333 @@ final class WP_Customize_Widgets {
 		wp_send_json_success( compact( 'form', 'instance' ) );
 	}
 
-	/***************************************************************************
+	/*
+	 * Selective Refresh Methods
+	 */
+
+	/**
+	 * Let sidebars_widgets and widget instance settings all have postMessage transport.
+	 *
+	 * The preview will determine whether or not the setting change requires a full refresh.
+	 *
+	 * @param array $args Setting args.
+	 * @return array
+	 */
+	public function filter_widget_customizer_setting_args( $args ) {
+		$args['transport'] = isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh';
+		return $args;
+	}
+
+	/**
+	 * Filter args for dynamic widget partials.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param array|false $partial_args Partial args.
+	 * @param string      $partial_id  Partial ID.
+	 * @return array Partial args
+	 */
+	public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
+
+		if ( preg_match( '/^widget\[.+\]$/', $partial_id ) ) {
+			if ( false === $partial_args ) {
+				$partial_args = array();
+			}
+			$partial_args = array_merge(
+				$partial_args,
+				array(
+					'type' => 'widget',
+					'render_callback' => array( $this, 'render_widget_partial' ),
+				)
+			);
+		}
+
+		return $partial_args;
+	}
+
+	/**
+	 * Add hooks for selective refresh.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 */
+	public function selective_refresh_init() {
+		if ( ! isset( $this->manager->selective_refresh ) ) {
+			return;
+		}
+
+		add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
+		add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
+		add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
+		add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
+		add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
+	}
+
+	/**
+	 * Enqueue scripts for the Customizer preview.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 */
+	public function customize_preview_enqueue_deps() {
+		if ( isset( $this->manager->selective_refresh ) ) {
+			$script = wp_scripts()->registered['customize-preview-widgets'];
+			$script->deps[] = 'customize-selective-refresh';
+		}
+
+		wp_enqueue_script( 'customize-preview-widgets' );
+		wp_enqueue_style( 'customize-preview' );
+		wp_enqueue_style( 'customize-partial-refresh-widgets-preview' );
+	}
+
+	/**
+	 * Keep track of the arguments that are being passed to the_widget().
+	 *
+	 * @param array $params {
+	 *     Dynamic sidebar params.
+	 *
+	 *     @type array $args        Sidebar args.
+	 *     @type array $widget_args Widget args.
+	 * }
+	 * @see WP_Customize_Nav_Menus_Partial_Refresh::filter_wp_nav_menu_args()
+	 *
+	 * @return array Params.
+	 */
+	public function filter_dynamic_sidebar_params( $params ) {
+		$sidebar_args = array_merge(
+			array(
+				'before_widget' => '',
+				'after_widget' => '',
+			),
+			$params[0]
+		);
+
+		// Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to.
+		$matches = array();
+		$is_valid = (
+			isset( $sidebar_args['id'] )
+			&&
+			is_registered_sidebar( $sidebar_args['id'] )
+			&&
+			( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] )
+			&&
+			preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches )
+		);
+		if ( ! $is_valid ) {
+			return $params;
+		}
+		$this->before_widget_tags_seen[ $matches['tag_name'] ] = true;
+
+		$context = array(
+			'sidebar_id' => $sidebar_args['id'],
+		);
+		if ( isset( $this->context_sidebar_instance_number ) ) {
+			$context['sidebar_instance_number'] = $this->context_sidebar_instance_number;
+		} else if ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) {
+			$context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ];
+		}
+
+		$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) );
+		$attributes .= ' data-customize-partial-type="widget"';
+		$attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) );
+		$attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) );
+		$sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] );
+
+		$params[0] = $sidebar_args;
+		return $params;
+	}
+
+	/**
+	 * List of the tag names seen for before_widget strings.
+	 *
+	 * This is used in the filter_wp_kses_allowed_html filter to ensure that the
+	 * data-* attributes can be whitelisted.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var array
+	 */
+	protected $before_widget_tags_seen = array();
+
+	/**
+	 * Ensure that the HTML data-* attributes for selective refresh are allowed by kses.
+	 *
+	 * This is needed in case the $before_widget is run through wp_kses() when printed.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 *
+	 * @param array $allowed_html Allowed HTML.
+	 * @return array Allowed HTML.
+	 */
+	function filter_wp_kses_allowed_data_attributes( $allowed_html ) {
+		foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) {
+			if ( ! isset( $allowed_html[ $tag_name ] ) ) {
+				$allowed_html[ $tag_name ] = array();
+			}
+			$allowed_html[ $tag_name ] = array_merge(
+				$allowed_html[ $tag_name ],
+				array_fill_keys( array(
+					'data-customize-partial-id',
+					'data-customize-partial-type',
+					'data-customize-partial-placement-context',
+					'data-customize-partial-widget-id',
+					'data-customize-partial-options',
+				), true )
+			);
+		}
+		return $allowed_html;
+	}
+
+	/**
+	 * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index.
+	 *
+	 * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var array
+	 */
+	protected $sidebar_instance_count = array();
+
+	/**
+	 * The current request's sidebar_instance_number context.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var int
+	 */
+	protected $context_sidebar_instance_number;
+
+	/**
+	 * Current sidebar ID being rendered.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var array
+	 */
+	protected $current_dynamic_sidebar_id_stack = array();
+
+	/**
+	 * Start keeping track of the current sidebar being rendered.
+	 *
+	 * Insert marker before widgets are rendered in a dynamic sidebar.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
+	 */
+	public function start_dynamic_sidebar( $index ) {
+		array_unshift( $this->current_dynamic_sidebar_id_stack, $index );
+		if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) {
+			$this->sidebar_instance_count[ $index ] = 0;
+		}
+		$this->sidebar_instance_count[ $index ] += 1;
+		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
+			printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
+		}
+	}
+
+	/**
+	 * Finish keeping track of the current sidebar being rendered.
+	 *
+	 * Insert marker after widgets are rendered in a dynamic sidebar.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
+	 */
+	public function end_dynamic_sidebar( $index ) {
+		assert( array_shift( $this->current_dynamic_sidebar_id_stack ) === $index );
+		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
+			printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
+		}
+	}
+
+	/**
+	 * Current sidebar being rendered.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var string
+	 */
+	protected $rendering_widget_id;
+
+	/**
+	 * Current widget being rendered.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var string
+	 */
+	protected $rendering_sidebar_id;
+
+	/**
+	 * Filter sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 *
+	 * @param array $sidebars_widgets Sidebars widgets.
+	 * @return array Sidebars widgets.
+	 */
+	public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) {
+		$sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id );
+		return $sidebars_widgets;
+	}
+
+	/**
+	 * Render a specific widget using the supplied sidebar arguments.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 *
+	 * @see dynamic_sidebar()
+	 *
+	 * @param WP_Customize_Partial $partial      Partial.
+	 * @param array                $context {
+	 *     Sidebar args supplied as container context.
+	 *
+	 *     @type string $sidebar_id                ID for sidebar for widget to render into.
+	 *     @type int    [$sidebar_instance_number] Disambiguating instance number.
+	 * }
+	 * @return string|false
+	 */
+	public function render_widget_partial( $partial, $context ) {
+		$id_data = $partial->id_data();
+		$widget_id = array_shift( $id_data['keys'] );
+		if ( ! is_array( $context ) || empty( $context['sidebar_id'] ) || ! is_registered_sidebar( $context['sidebar_id'] ) ) {
+			return false;
+		}
+		$this->rendering_sidebar_id = $context['sidebar_id'];
+
+		if ( isset( $context['sidebar_instance_number'] ) ) {
+			$this->context_sidebar_instance_number = intval( $context['sidebar_instance_number'] );
+		}
+
+		// Filter sidebars_widgets so that only the queried widget is in the sidebar.
+		$this->rendering_widget_id = $widget_id;
+
+		$filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' );
+		add_filter( 'sidebars_widgets', $filter_callback, 1000 );
+
+		// Render the widget.
+		ob_start();
+		dynamic_sidebar( $this->rendering_sidebar_id = $context['sidebar_id'] );
+		$container = ob_get_clean();
+
+		// Reset variables for next partial render.
+		remove_filter( 'sidebars_widgets', $filter_callback, 1000 );
+		$this->context_sidebar_instance_number = null;
+		$this->rendering_sidebar_id = null;
+		$this->rendering_widget_id = null;
+
+		return $container;
+	}
+
+	/*
 	 * Option Update Capturing
-	 ***************************************************************************/
+	 */
 
 	/**
 	 * List of captured widget option updates.
@@ -1611,7 +1941,7 @@ final class WP_Customize_Widgets {
 			return;
 		}
 
-		remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
+		remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 );
 
 		foreach ( array_keys( $this->_captured_options ) as $option_name ) {
 			remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
diff --git src/wp-includes/css/customize-preview.css src/wp-includes/css/customize-preview.css
index bc4a6fe..75251ea 100644
--- src/wp-includes/css/customize-preview.css
+++ src/wp-includes/css/customize-preview.css
@@ -4,3 +4,9 @@
 	transition: opacity 0.25s;
 	cursor: progress;
 }
+
+/* Override highlight when refreshing */
+.customize-partial-refreshing.widget-customizer-highlighted-widget {
+	-webkit-box-shadow: none;
+	box-shadow: none;
+}
diff --git src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
index 073423e..ddfd47b 100644
--- src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
+++ src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
@@ -70,7 +70,7 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
 	 * @access public
 	 * @var string
 	 */
-	public $transport = 'postMessage';
+	public $transport = 'refresh';
 
 	/**
 	 * The post ID represented by this setting instance. This is the db_id.
diff --git src/wp-includes/customize/class-wp-customize-partial.php src/wp-includes/customize/class-wp-customize-partial.php
new file mode 100644
index 0000000..a481250
--- /dev/null
+++ src/wp-includes/customize/class-wp-customize-partial.php
@@ -0,0 +1,279 @@
+<?php
+/**
+ * WordPress Customize Partial class
+ *
+ * @package WordPress
+ * @subpackage Customize
+ * @since 4.5.0
+ */
+
+/**
+ * Customize Partial class.
+ *
+ * Representation of a rendered region in the previewed page that gets
+ * selectively refreshed when an associated setting is changed.
+ * This class is analogous of WP_Customize_Control.
+ *
+ * @since 4.5.0
+ */
+class WP_Customize_Partial {
+
+	/**
+	 * Component.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @var WP_Customize_Selective_Refresh
+	 */
+	public $component;
+
+	/**
+	 * Unique identifier for the partial.
+	 *
+	 * If the partial is used to display a single setting, this would generally
+	 * be the same as the associated setting's ID.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @var string
+	 */
+	public $id;
+
+	/**
+	 * Parsed ID.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var array {
+	 *     @type string $base ID base.
+	 *     @type array  $keys Keys for multidimensional.
+	 * }
+	 */
+	protected $id_data = array();
+
+	/**
+	 * Type of this partial.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @var string
+	 */
+	public $type = 'default';
+
+	/**
+	 * The jQuery selector to find the container element for the partial.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @var string
+	 */
+	public $selector;
+
+	/**
+	 * All settings tied to the partial.
+	 *
+	 * @access public
+	 * @since 4.5.0
+	 * @var WP_Customize_Setting[]
+	 */
+	public $settings;
+
+	/**
+	 * The ID for the setting that this partial is primarily responsible for rendering.
+	 *
+	 * If not supplied, it will default to the ID of the first setting.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @var string
+	 */
+	public $primary_setting;
+
+	/**
+	 * Render callback.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @see WP_Customize_Partial::render()
+	 * @var callable Callback is called with one argument, the instance of
+	 *                 WP_Customize_Partial. The callback can either echo the
+	 *                 partial or return the partial as a string, or return false if error.
+	 */
+	public $render_callback;
+
+	/**
+	 * Whether the container element is included in the partial, or if only the contents are rendered.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @var bool
+	 */
+	public $container_inclusive = false;
+
+	/**
+	 * Whether to refresh the entire preview in case a partial cannot be refreshed.
+	 *
+	 * A partial render is considered a failure if the render_callback returns false.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @var bool
+	 */
+	public $fallback_refresh = true;
+
+	/**
+	 * Constructor.
+	 *
+	 * Supplied `$args` override class property defaults.
+	 *
+	 * If `$args['settings']` is not defined, use the $id as the setting ID.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param WP_Customize_Selective_Refresh $component Customize Partial Refresh plugin instance.
+	 * @param string                         $id        Control ID.
+	 * @param array                          $args      {
+	 *     Optional. Arguments to override class property defaults.
+	 *
+	 *     @type array|string  $settings        All settings IDs tied to the partial. If undefined, `$id` will be used.
+	 * }
+	 */
+	public function __construct( WP_Customize_Selective_Refresh $component, $id, $args = array() ) {
+		$keys = array_keys( get_object_vars( $this ) );
+		foreach ( $keys as $key ) {
+			if ( isset( $args[ $key ] ) ) {
+				$this->$key = $args[ $key ];
+			}
+		}
+
+		$this->component = $component;
+		$this->id = $id;
+		$this->id_data['keys'] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
+		$this->id_data['base'] = array_shift( $this->id_data['keys'] );
+
+		if ( empty( $this->render_callback ) ) {
+			$this->render_callback = array( $this, 'render_callback' );
+		}
+
+		// Process settings.
+		if ( empty( $this->settings ) ) {
+			$this->settings = array( $id );
+		} else if ( ! is_array( $this->settings ) ) {
+			$this->settings = array( $this->settings );
+		}
+		if ( empty( $this->primary_setting ) ) {
+			$this->primary_setting = current( $this->settings );
+		}
+	}
+
+	/**
+	 * Get parsed ID data for multidimensional setting.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 *
+	 * @return array {
+	 *     ID data for multidimensional partial.
+	 *
+	 *     @type string $base ID base.
+	 *     @type array  $keys Keys for multidimensional array.
+	 * }
+	 */
+	final public function id_data() {
+		return $this->id_data;
+	}
+
+	/**
+	 * Render the template partial involving the associated settings.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 *
+	 * @param array $container_context Optional array of context data associated with the target container.
+	 * @return string|array|false The rendered partial as a string, raw data array (for client-side JS template), or false if no render applied.
+	 */
+	final public function render( $container_context = array() ) {
+		$partial = $this;
+
+		$rendered = false;
+		if ( ! empty( $this->render_callback ) ) {
+			ob_start();
+			$return_render = call_user_func( $this->render_callback, $this, $container_context );
+			$ob_render = ob_get_clean();
+
+			if ( null !== $return_render && '' !== $ob_render ) {
+				_doing_it_wrong( __FUNCTION__, __( 'Partial render must echo the content or return the content string (or array), but not both.' ), '4.5.0' );
+			}
+
+			// Note that the string return takes precedence because the $ob_render may just include PHP warnings or notices.
+			if ( null !== $return_render ) {
+				$rendered = $return_render;
+			} else {
+				$rendered = $ob_render;
+			}
+		}
+
+		/**
+		 * Filter partial rendering.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param string|array|false   $rendered          The partial value. Default false.
+		 * @param WP_Customize_Partial $partial           WP_Customize_Setting instance.
+		 * @param array                $container_context Optional array of context data associated with the target container.
+		 */
+		$rendered = apply_filters( 'customize_partial_render', $rendered, $partial, $container_context );
+
+		/**
+		 * Filter partial rendering by the partial ID.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param string|array|false   $rendered          The partial value. Default false.
+		 * @param WP_Customize_Partial $partial           WP_Customize_Setting instance.
+		 * @param array                $container_context Optional array of context data associated with the target container.
+		 */
+		$rendered = apply_filters( "customize_partial_render_{$partial->id}", $rendered, $partial, $container_context );
+
+		return $rendered;
+	}
+
+	/**
+	 * Default callback used when invoking WP_Customize_Control::render().
+	 *
+	 * Note that this method may echo the partial *or* return the partial as
+	 * a string or array, but not both. Output buffering is performed when this
+	 * is called. Subclasses can override this with their specific logic, or they
+	 * may provide an 'render_callback' argument to the constructor.
+	 *
+	 * This method may return an HTML string for straight DOM injection, or it
+	 * may return an array for supporting Partial JS subclasses to render by
+	 * applying to client-side templating.
+	 *
+	 * @access public
+	 * @since 4.5.0
+	 *
+	 * @return string|array|false
+	 */
+	public function render_callback() {
+		return false;
+	}
+
+	/**
+	 * Get the data to export to the client via JSON.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @return array Array of parameters passed to the JavaScript.
+	 */
+	public function json() {
+		$exports = array();
+		$exports['settings'] = $this->settings;
+		$exports['primarySetting'] = $this->primary_setting;
+		$exports['selector'] = $this->selector;
+		$exports['type'] = $this->type;
+		$exports['fallbackRefresh'] = $this->fallback_refresh;
+		$exports['containerInclusive'] = $this->container_inclusive;
+		return $exports;
+	}
+}
diff --git src/wp-includes/customize/class-wp-customize-selective-refresh.php src/wp-includes/customize/class-wp-customize-selective-refresh.php
new file mode 100644
index 0000000..f7587eb
--- /dev/null
+++ src/wp-includes/customize/class-wp-customize-selective-refresh.php
@@ -0,0 +1,425 @@
+<?php
+/**
+ * WordPress Customize Selective Refresh class
+ *
+ * @package WordPress
+ * @subpackage Customize
+ * @since 4.5.0
+ */
+
+/**
+ * WordPress Customize Selective Refresh class
+ *
+ * @since 4.5.0
+ */
+class WP_Customize_Selective_Refresh {
+
+	/**
+	 * Query var used in requests to render partials.
+	 *
+	 * @since 4.5.0
+	 */
+	const RENDER_QUERY_VAR = 'wp_customize_render_partials';
+
+	/**
+	 * Customize manager.
+	 *
+	 * @var WP_Customize_Manager
+	 */
+	public $manager;
+
+	/**
+	 * Plugin bootstrap for Partial Refresh functionality.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param WP_Customize_Manager $manager Manager.
+	 */
+	public function __construct( WP_Customize_Manager $manager ) {
+		$this->manager = $manager;
+		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-partial.php' );
+
+		add_action( 'customize_controls_print_footer_scripts', array( $this, 'enqueue_pane_scripts' ) );
+		add_action( 'customize_preview_init', array( $this, 'init_preview' ) );
+	}
+
+	/**
+	 * Registered instances of WP_Customize_Partial.
+	 *
+	 * @since 4.5.0
+	 * @access protected
+	 * @var WP_Customize_Partial[]
+	 */
+	protected $partials = array();
+
+	/**
+	 * Get the registered partials.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 *
+	 * @return WP_Customize_Partial[] Partials.
+	 */
+	public function partials() {
+		return $this->partials;
+	}
+
+	/**
+	 * Add a customize partial.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 *
+	 * @param WP_Customize_Partial|string $id   Customize Partial object, or Panel ID.
+	 * @param array                       $args Optional. Partial arguments. Default empty array.
+	 *
+	 * @return WP_Customize_Partial             The instance of the panel that was added.
+	 */
+	public function add_partial( $id, $args = array() ) {
+		if ( $id instanceof WP_Customize_Partial ) {
+			$partial = $id;
+		} else {
+			$class = 'WP_Customize_Partial';
+
+			/** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */
+			$args = apply_filters( 'customize_dynamic_partial_args', $args, $id );
+
+			/** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */
+			$class = apply_filters( 'customize_dynamic_partial_class', $class, $id, $args );
+
+			$partial = new $class( $this, $id, $args );
+		}
+
+		$this->partials[ $partial->id ] = $partial;
+		return $partial;
+	}
+
+	/**
+	 * Retrieve a customize partial.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 *
+	 * @param string $id Customize Partial ID.
+	 * @return WP_Customize_Partial|null The partial, if set.
+	 */
+	public function get_partial( $id ) {
+		if ( isset( $this->partials[ $id ] ) ) {
+			return $this->partials[ $id ];
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Remove a customize partial.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 *
+	 * @param string $id Customize Partial ID.
+	 */
+	public function remove_partial( $id ) {
+		unset( $this->partials[ $id ] );
+	}
+
+	/**
+	 * Initialize Customizer preview.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 */
+	public function init_preview() {
+		add_action( 'template_redirect', array( $this, 'handle_render_partials_request' ) );
+		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );
+	}
+
+	/**
+	 * Enqueue pane scripts.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 */
+	public function enqueue_pane_scripts() {
+		wp_enqueue_script( 'customize-controls-hacks' );
+	}
+
+	/**
+	 * Enqueue preview scripts.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 */
+	public function enqueue_preview_scripts() {
+		wp_enqueue_script( 'customize-selective-refresh' );
+		add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1000 );
+	}
+
+	/**
+	 * Export data in preview after it has finished rendering so that partials can be added at runtime.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 */
+	public function export_preview_data() {
+
+		$partials = array();
+		foreach ( $this->partials() as $partial ) {
+			$partials[ $partial->id ] = $partial->json();
+		}
+
+		$exports = array(
+			'partials'       => $partials,
+			'renderQueryVar' => self::RENDER_QUERY_VAR,
+			'l10n'           => array(
+				'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
+			),
+		);
+
+		// Export data to JS.
+		echo sprintf( '<script>var _customizePartialRefreshExports = %s;</script>', wp_json_encode( $exports ) );
+	}
+
+	/**
+	 * Register dynamically-created partials.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 * @see WP_Customize_Manager::add_dynamic_settings()
+	 *
+	 * @param array $partial_ids         The partial ID to add.
+	 * @return array                     Added WP_Customize_Partial instances.
+	 */
+	public function add_dynamic_partials( $partial_ids ) {
+		$new_partials = array();
+
+		foreach ( $partial_ids as $partial_id ) {
+
+			// Skip partials already created.
+			$partial = $this->get_partial( $partial_id );
+			if ( $partial ) {
+				continue;
+			}
+
+			$partial_args = false;
+			$partial_class = 'WP_Customize_Partial';
+
+			/**
+			 * Filter a dynamic partial's constructor args.
+			 *
+			 * For a dynamic partial to be registered, this filter must be employed
+			 * to override the default false value with an array of args to pass to
+			 * the WP_Customize_Partial constructor.
+			 *
+			 * @since 4.5.0
+			 *
+			 * @param false|array $partial_args The arguments to the WP_Customize_Partial constructor.
+			 * @param string      $partial_id   ID for dynamic partial.
+			 */
+			$partial_args = apply_filters( 'customize_dynamic_partial_args', $partial_args, $partial_id );
+			if ( false === $partial_args ) {
+				continue;
+			}
+
+			/**
+			 * Allow non-statically created partials to be constructed with custom WP_Customize_Partial subclass.
+			 *
+			 * @since 4.5.0
+			 *
+			 * @param string $partial_class WP_Customize_Partial or a subclass.
+			 * @param string $partial_id    ID for dynamic partial.
+			 * @param array  $partial_args  The arguments to the WP_Customize_Partial constructor.
+			 */
+			$partial_class = apply_filters( 'customize_dynamic_partial_class', $partial_class, $partial_id, $partial_args );
+
+			$partial = new $partial_class( $this, $partial_id, $partial_args );
+
+			$this->add_partial( $partial );
+			$new_partials[] = $partial;
+		}
+		return $new_partials;
+	}
+
+	/**
+	 * Check whether the request is for rendering partials.
+	 *
+	 * Note that this will not consider whether the request is authorized or valid,
+	 * just that essentially the route is a match.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 *
+	 * @return bool Whether the request is for rendering partials.
+	 */
+	public function is_render_partials_request() {
+		return ! empty( $_POST[ self::RENDER_QUERY_VAR ] );
+	}
+
+	/**
+	 * Log of errors triggered when partials are rendered.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var array
+	 */
+	protected $triggered_errors = array();
+
+	/**
+	 * Keep track of the current partial being rendered.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 * @var string
+	 */
+	protected $current_partial_id;
+
+	/**
+	 * Handle PHP error triggered during rendering the partials.
+	 *
+	 * These errors will be relayed back to the client in the Ajax response.
+	 *
+	 * @since 4.5.0
+	 * @access private
+	 *
+	 * @param int    $errno   Error number.
+	 * @param string $errstr  Error string.
+	 * @param string $errfile Error file.
+	 * @param string $errline Error line.
+	 * @return bool
+	 */
+	public function handle_error( $errno, $errstr, $errfile = null, $errline = null ) {
+		$this->triggered_errors[] = array(
+			'partial' => $this->current_partial_id,
+			'error_number' => $errno,
+			'error_string' => $errstr,
+			'error_file' => $errfile,
+			'error_line' => $errline,
+		);
+		return true;
+	}
+
+	/**
+	 * Handle Ajax request to return the rendered partials for the requested placements.
+	 *
+	 * @since 4.5.0
+	 * @access public
+	 */
+	public function handle_render_partials_request() {
+		if ( ! $this->is_render_partials_request() ) {
+			return;
+		}
+
+		$this->manager->remove_preview_signature();
+
+		/*
+		 * Note that is_customize_preview() returning true will entail that the
+		 * user passed the 'customize' capability check and the nonce check, since
+		 * WP_Customize_Manager::setup_theme() is where the previewing flag is set.
+		 */
+		if ( ! is_customize_preview() ) {
+			status_header( 403 );
+			wp_send_json_error( 'expected_customize_preview' );
+		} else if ( ! isset( $_POST['partials'] ) ) {
+			status_header( 400 );
+			wp_send_json_error( 'missing_partials' );
+		}
+		$partials = json_decode( wp_unslash( $_POST['partials'] ), true );
+		if ( ! is_array( $partials ) ) {
+			wp_send_json_error( 'malformed_partials' );
+		}
+		$this->add_dynamic_partials( array_keys( $partials ) );
+
+		/**
+		 * Do setup before rendering each partial.
+		 *
+		 * Plugins may do things like call <code>wp_enqueue_scripts()</code> and
+		 * gather a list of the scripts and styles which may get enqueued in the response.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.
+		 * @param array                          $partials IDs for the partials to render in the request.
+		 */
+		do_action( 'customize_render_partials_before', $this, $partials );
+
+		set_error_handler( array( $this, 'handle_error' ), error_reporting() );
+		$contents = array();
+		foreach ( $partials as $partial_id => $container_contexts ) {
+			$this->current_partial_id = $partial_id;
+			if ( ! is_array( $container_contexts ) ) {
+				wp_send_json_error( 'malformed_container_contexts' );
+			}
+
+			$partial = $this->get_partial( $partial_id );
+			if ( ! $partial ) {
+				$contents[ $partial_id ] = null;
+				continue;
+			}
+
+			$contents[ $partial_id ] = array();
+
+			// @todo The array should include not only the contents, but also whether the container is included?
+			if ( empty( $container_contexts ) ) {
+				// Since there are no container contexts, render just once.
+				$contents[ $partial_id ][] = $partial->render( null );
+			} else {
+				foreach ( $container_contexts as $container_context ) {
+					$contents[ $partial_id ][] = $partial->render( $container_context );
+				}
+			}
+		}
+		$this->current_partial_id = null;
+		restore_error_handler();
+
+		/**
+		 * Do finalization after rendering each partial.
+		 *
+		 * Plugins may do things like call <code>wp_footer()</code> to scrape scripts output and
+		 * return them via the customize_render_partials_response filter.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.
+		 * @param array                          $partials IDs for the partials to rendered in the request.
+		 */
+		do_action( 'customize_render_partials_after', $this, $partials );
+
+		$response = array(
+			'contents' => $contents,
+		);
+		if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
+			$response['errors'] = $this->triggered_errors;
+		}
+
+		/**
+		 * Filter the response from rendering the partials.
+		 *
+		 * Plugins may use this filter to inject <code>$scripts</code> and
+		 * <code>$styles</code> which are dependencies for the partials being
+		 * rendered. The response data will be available to the client via the
+		 * <code>render-partials-response</code> JS event, so the client can then
+		 * inject the scripts and styles into the DOM if they have not already
+		 * been enqueued there. If plugins do this, they'll need to take care
+		 * for any scripts that do <code>document.write()</code> and make sure
+		 * that these are not injected, or else to override the function to no-op,
+		 * or else the page will be destroyed.
+		 *
+		 * Plugins should be aware that <code>$scripts</code> and <code>$styles</code>
+		 * these may eventually be included by default in the response.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param array $response {
+		 *     Response.
+		 *
+		 *     @type array $contents  Associative array mapping a partial ID its corresponding array of contents for the containers requested.
+		 *     @type array [$errors]  List of errors triggered during rendering of partials, if WP_DEBUG_DISPLAY is enabled.
+		 * }
+		 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.
+		 * @param array                          $partials IDs for the partials to rendered in the request.
+		 */
+		$response = apply_filters( 'customize_render_partials_response', $response, $this, $partials );
+
+		wp_send_json_success( $response );
+	}
+}
diff --git src/wp-includes/js/customize-preview-nav-menus.js src/wp-includes/js/customize-preview-nav-menus.js
index 9e84494..c61e620 100644
--- src/wp-includes/js/customize-preview-nav-menus.js
+++ src/wp-includes/js/customize-preview-nav-menus.js
@@ -1,315 +1,197 @@
-/* global JSON, _wpCustomizePreviewNavMenusExports */
-
-( function( $, _, wp ) {
+wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
 	'use strict';
 
-	if ( ! wp || ! wp.customize ) { return; }
-
-	var api = wp.customize,
-		currentRefreshDebounced = {},
-		refreshDebounceDelay = 200,
-		settings = {},
-		defaultSettings = {
-			renderQueryVar: null,
-			renderNonceValue: null,
-			renderNoncePostKey: null,
-			requestUri: '/',
-			navMenuInstanceArgs: {},
-			l10n: {}
-		};
-
-	api.MenusCustomizerPreview = {
-		/**
-		 * Bootstrap functionality.
-		 */
-		init : function() {
-			var self = this, initializedSettings = {};
-
-			settings = _.extend( {}, defaultSettings );
-			if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
-				_.extend( settings, _wpCustomizePreviewNavMenusExports );
-			}
-
-			api.each( function( setting, id ) {
-				setting.id = id;
-				initializedSettings[ setting.id ] = true;
-				self.bindListener( setting );
-			} );
-
-			api.preview.bind( 'setting', function( args ) {
-				var id, value, setting;
-				args = args.slice();
-				id = args.shift();
-				value = args.shift();
+	var self = {};
 
-				setting = api( id );
-				if ( ! setting ) {
-					// Currently customize-preview.js is not creating settings for dynamically-created settings in the pane, so we have to do it.
-					setting = api.create( id, value ); // @todo This should be in core
-				}
-				if ( ! setting.id ) {
-					// Currently customize-preview.js doesn't set the id property for each setting, like customize-controls.js does.
-					setting.id = id;
-				}
+	/**
+	 * Initialize nav menus preview.
+	 */
+	self.init = function() {
+		var self = this;
 
-				if ( ! initializedSettings[ setting.id ] ) {
-					initializedSettings[ setting.id ] = true;
-					if ( self.bindListener( setting ) ) {
-						setting.callbacks.fireWith( setting, [ setting(), null ] );
-					}
-				}
-			} );
+		if ( api.selectiveRefresh ) {
+			self.watchNavMenuLocationChanges();
+		}
 
+		api.preview.bind( 'active', function() {
 			self.highlightControls();
-		},
-
-		/**
-		 *
-		 * @param {wp.customize.Value} setting
-		 * @returns {boolean} Whether the setting was bound.
-		 */
-		bindListener : function( setting ) {
-			var matches, themeLocation;
-
-			matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
-			if ( matches ) {
-				setting.navMenuId = parseInt( matches[1], 10 );
-				setting.bind( this.onChangeNavMenuSetting );
-				return true;
-			}
-
-			matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
-			if ( matches ) {
-				setting.navMenuItemId = parseInt( matches[1], 10 );
-				setting.bind( this.onChangeNavMenuItemSetting );
-				return true;
-			}
-
-			matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
-			if ( matches ) {
-				themeLocation = matches[1];
-				setting.bind( _.bind( function() {
-					this.refreshMenuLocation( themeLocation );
-				}, this ) );
-				return true;
-			}
-
-			return false;
-		},
-
-		/**
-		 * Handle changing of a nav_menu setting.
-		 *
-		 * @this {wp.customize.Setting}
-		 */
-		onChangeNavMenuSetting : function() {
-			var setting = this;
-			if ( ! setting.navMenuId ) {
-				throw new Error( 'Expected navMenuId property to be set.' );
-			}
-			api.MenusCustomizerPreview.refreshMenu( setting.navMenuId );
-		},
+		} );
+	};
 
-		/**
-		 * Handle changing of a nav_menu_item setting.
-		 *
-		 * @this {wp.customize.Setting}
-		 * @param {object} to
-		 * @param {object} from
-		 */
-		onChangeNavMenuItemSetting : function( to, from ) {
-			if ( from && from.nav_menu_term_id && ( ! to || from.nav_menu_term_id !== to.nav_menu_term_id ) ) {
-				api.MenusCustomizerPreview.refreshMenu( from.nav_menu_term_id );
-			}
-			if ( to && to.nav_menu_term_id ) {
-				api.MenusCustomizerPreview.refreshMenu( to.nav_menu_term_id );
-			}
-		},
+	if ( api.selectiveRefresh ) {
 
 		/**
-		 * Update a given menu rendered in the preview.
+		 * Partial representing an invocation of wp_nav_menu().
 		 *
-		 * @param {int} menuId
+		 * @class
+		 * @augments wp.customize.selectiveRefresh.Partial
+		 * @since 4.5.0
 		 */
-		refreshMenu : function( menuId ) {
-			var assignedLocations = [];
-
-			api.each(function( setting, id ) {
-				var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
-				if ( matches && menuId === setting() ) {
-					assignedLocations.push( matches[1] );
+		self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({
+
+			/**
+			 * Constructor.
+			 *
+			 * @since 4.5.0
+			 * @param {string} id - Partial ID.
+			 * @param {Object} options
+			 * @param {Object} options.params
+			 * @param {Object} options.params.navMenuArgs
+			 * @param {string} options.params.navMenuArgs.args_hmac
+			 * @param {string} [options.params.navMenuArgs.theme_location]
+			 * @param {number} [options.params.navMenuArgs.menu]
+			 * @param {object} [options.constructingContainerContext]
+			 */
+			initialize: function( id, options ) {
+				var partial = this, matches, argsHmac;
+				matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ );
+				if ( ! matches ) {
+					throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' );
 				}
-			});
-
-			_.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
-				if ( menuId === navMenuArgs.menu || -1 !== _.indexOf( assignedLocations, navMenuArgs.theme_location ) ) {
-					this.refreshMenuInstanceDebounced( instanceNumber );
+				argsHmac = matches[1];
+
+				options = options || {};
+				options.params = _.extend(
+					{
+						selector: '[data-customize-partial-id="' + id + '"]',
+						navMenuArgs: options.constructingContainerContext || {},
+						containerInclusive: true
+					},
+					options.params || {}
+				);
+				api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
+
+				if ( ! _.isObject( partial.params.navMenuArgs ) ) {
+					throw new Error( 'Missing navMenuArgs' );
 				}
-			}, this );
-		},
-
-		/**
-		 * Refresh the menu(s) associated with a given nav menu location.
-		 *
-		 * @param {string} location
-		 */
-		refreshMenuLocation : function( location ) {
-			var foundInstance = false;
-			_.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
-				if ( location === navMenuArgs.theme_location ) {
-					this.refreshMenuInstanceDebounced( instanceNumber );
-					foundInstance = true;
+				if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
+					throw new Error( 'args_hmac mismatch with id' );
 				}
-			}, this );
-			if ( ! foundInstance ) {
-				api.preview.send( 'refresh' );
-			}
-		},
-
-		/**
-		 * Update a specific instance of a given menu on the page.
-		 *
-		 * @param {int} instanceNumber
-		 */
-		refreshMenuInstance : function( instanceNumber ) {
-			var data, menuId, customized, container, request, wpNavMenuArgs, instance, containerInstanceClassName;
-
-			if ( ! settings.navMenuInstanceArgs[ instanceNumber ] ) {
-				throw new Error( 'unknown_instance_number' );
-			}
-			instance = settings.navMenuInstanceArgs[ instanceNumber ];
-
-			containerInstanceClassName = 'partial-refreshable-nav-menu-' + String( instanceNumber );
-			container = $( '.' + containerInstanceClassName );
-
-			if ( _.isNumber( instance.menu ) ) {
-				menuId = instance.menu;
-			} else if ( instance.theme_location && api.has( 'nav_menu_locations[' + instance.theme_location + ']' ) ) {
-				menuId = api( 'nav_menu_locations[' + instance.theme_location + ']' ).get();
-			}
-
-			if ( ! menuId || ! instance.can_partial_refresh || 0 === container.length ) {
-				api.preview.send( 'refresh' );
-				return;
-			}
-			menuId = parseInt( menuId, 10 );
-
-			data = {
-				nonce: wp.customize.settings.nonce.preview,
-				wp_customize: 'on'
-			};
-			if ( ! wp.customize.settings.theme.active ) {
-				data.theme = wp.customize.settings.theme.stylesheet;
-			}
-			data[ settings.renderQueryVar ] = '1';
-
-			// Gather settings to send in partial refresh request.
-			customized = {};
-			api.each( function( setting, id ) {
-				var value = setting.get(), shouldSend = false;
-				// @todo Core should propagate the dirty state into the Preview as well so we can use that here.
-
-				// Send setting if it is a nav_menu_locations[] setting.
-				shouldSend = shouldSend || /^nav_menu_locations\[/.test( id );
-
-				// Send setting if it is the setting for this menu.
-				shouldSend = shouldSend || id === 'nav_menu[' + String( menuId ) + ']';
-
-				// Send setting if it is one that is associated with this menu, or it is deleted.
-				shouldSend = shouldSend || ( /^nav_menu_item\[/.test( id ) && ( false === value || menuId === value.nav_menu_term_id ) );
-
-				if ( shouldSend ) {
-					customized[ id ] = value;
+			},
+
+			/**
+			 * Return whether the setting is related to this partial.
+			 *
+			 * @since 4.5.0
+			 * @param {wp.customize.Value|string} setting  - Object or ID.
+			 * @param {number|object|false|null}  newValue - New value, or null if the setting was just removed.
+			 * @param {number|object|false|null}  oldValue - Old value, or null if the setting was just added.
+			 * @returns {boolean}
+			 */
+			isRelatedSetting: function( setting, newValue, oldValue ) {
+				var partial = this, navMenuLocationSetting, navMenuId;
+				if ( _.isString( setting ) ) {
+					setting = api( setting );
 				}
-			} );
-			data.customized = JSON.stringify( customized );
-			data[ settings.renderNoncePostKey ] = settings.renderNonceValue;
 
-			wpNavMenuArgs = $.extend( {}, instance );
-			data.wp_nav_menu_args_hash = wpNavMenuArgs.args_hash;
-			delete wpNavMenuArgs.args_hash;
-			data.wp_nav_menu_args = JSON.stringify( wpNavMenuArgs );
-
-			container.addClass( 'customize-partial-refreshing' );
-
-			request = wp.ajax.send( null, {
-				data: data,
-				url: api.settings.url.self
-			} );
-			request.done( function( data ) {
-				// If the menu is now not visible, refresh since the page layout may have changed.
-				if ( false === data ) {
-					api.preview.send( 'refresh' );
-					return;
+				if ( partial.params.navMenuArgs.theme_location ) {
+					if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
+						return true;
+					}
+					navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
 				}
 
-				var eventParam, previousContainer = container;
-				container = $( data );
-				container.addClass( containerInstanceClassName );
-				container.addClass( 'partial-refreshable-nav-menu customize-partial-refreshing' );
-				previousContainer.replaceWith( container );
-				eventParam = {
-					instanceNumber: instanceNumber,
-					wpNavArgs: wpNavMenuArgs, // @deprecated
-					wpNavMenuArgs: wpNavMenuArgs,
-					oldContainer: previousContainer,
-					newContainer: container
-				};
-				container.removeClass( 'customize-partial-refreshing' );
-				$( document ).trigger( 'customize-preview-menu-refreshed', [ eventParam ] );
-			} );
-			request.fail( function() {
-				api.preview.send( 'refresh' );
-			} );
-		},
+				navMenuId = partial.params.navMenuArgs.menu;
+				if ( ! navMenuId && navMenuLocationSetting ) {
+					navMenuId = navMenuLocationSetting();
+				}
 
-		refreshMenuInstanceDebounced : function( instanceNumber ) {
-			if ( currentRefreshDebounced[ instanceNumber ] ) {
-				clearTimeout( currentRefreshDebounced[ instanceNumber ] );
+				if ( ! navMenuId ) {
+					return false;
+				}
+				return (
+					( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
+					( /^nav_menu_item\[/.test( setting.id ) &&
+						( ( newValue && newValue.nav_menu_term_id === navMenuId ) || ( oldValue && oldValue.nav_menu_term_id === navMenuId ) )
+					)
+				);
+			},
+
+			/**
+			 * Render content.
+			 *
+			 * @inheritdoc
+			 * @param {wp.customize.selectiveRefresh.Placement} placement
+			 */
+			renderContent: function( placement ) {
+				var partial = this, previousContainer = placement.container;
+				if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
+
+					// Trigger deprecated event.
+					$( document ).trigger( 'customize-preview-menu-refreshed', [ {
+						instanceNumber: null, // @deprecated
+						wpNavArgs: placement.context, // @deprecated
+						wpNavMenuArgs: placement.context,
+						oldContainer: previousContainer,
+						newContainer: placement.container
+					} ] );
+				}
 			}
-			currentRefreshDebounced[ instanceNumber ] = setTimeout(
-				_.bind( function() {
-					this.refreshMenuInstance( instanceNumber );
-				}, this ),
-				refreshDebounceDelay
-			);
-		},
+		});
+
+		api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
 
 		/**
-		 * Connect nav menu items with their corresponding controls in the pane.
+		 * Watch for changes to nav_menu_locations[] settings.
+		 *
+		 * Refresh partials associated with the given nav_menu_locations[] setting,
+		 * or request an entire preview refresh if there are no containers in the
+		 * document for a partial associated with the theme location.
+		 *
+		 * @since 4.5.0
 		 */
-		highlightControls: function() {
-			var selector = '.menu-item',
-				addTooltips;
-
-			// Open expand the menu item control when shift+clicking the menu item
-			$( document ).on( 'click', selector, function( e ) {
-				var navMenuItemParts;
-				if ( ! e.shiftKey ) {
+		self.watchNavMenuLocationChanges = function() {
+			api.bind( 'change', function( setting ) {
+				var themeLocation, themeLocationPartialFound = false, matches = setting.id.match( /^nav_menu_locations\[(.+)]$/ );
+				if ( ! matches ) {
 					return;
 				}
+				themeLocation = matches[1];
+				api.selectiveRefresh.partial.each( function( partial ) {
+					if ( partial.extended( self.NavMenuInstancePartial ) && partial.params.navMenuArgs.theme_location === themeLocation ) {
+						partial.refresh();
+						themeLocationPartialFound = true;
+					}
+				} );
 
-				navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
-				if ( navMenuItemParts ) {
-					e.preventDefault();
-					e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
-					api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
+				if ( ! themeLocationPartialFound ) {
+					api.selectiveRefresh.requestFullRefresh();
 				}
-			});
-
-			addTooltips = function( e, params ) {
-				params.newContainer.find( selector ).attr( 'title', settings.l10n.editNavMenuItemTooltip );
-			};
+			} );
+		};
+	}
+
+	/**
+	 * Connect nav menu items with their corresponding controls in the pane.
+	 *
+	 * Setup shift-click on nav menu items which are more granular than the nav menu partial itself.
+	 * Also this applies even if a nav menu is not partial-refreshable.
+	 *
+	 * @since 4.5.0
+	 */
+	self.highlightControls = function() {
+		var selector = '.menu-item';
+
+		// Focus on the menu item control when shift+clicking the menu item.
+		$( document ).on( 'click', selector, function( e ) {
+			var navMenuItemParts;
+			if ( ! e.shiftKey ) {
+				return;
+			}
 
-			addTooltips( null, { newContainer: $( document.body ) } );
-			$( document ).on( 'customize-preview-menu-refreshed', addTooltips );
-		}
+			navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
+			if ( navMenuItemParts ) {
+				e.preventDefault();
+				e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
+				api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
+			}
+		});
 	};
 
 	api.bind( 'preview-ready', function() {
-		api.preview.bind( 'active', function() {
-			api.MenusCustomizerPreview.init();
-		} );
+		self.init();
 	} );
 
-}( jQuery, _, wp ) );
+	return self;
+
+}( jQuery, _, wp, wp.customize ) );
diff --git src/wp-includes/js/customize-preview-widgets.js src/wp-includes/js/customize-preview-widgets.js
index f982829..92e7732 100644
--- src/wp-includes/js/customize-preview-widgets.js
+++ src/wp-includes/js/customize-preview-widgets.js
@@ -1,119 +1,648 @@
-(function( wp, $ ){
+/* global _wpWidgetCustomizerPreviewSettings */
+wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
 
-	if ( ! wp || ! wp.customize ) { return; }
+	var self;
 
-	var api = wp.customize;
+	self = {
+		renderedSidebars: {},
+		renderedWidgets: {},
+		registeredSidebars: [],
+		registeredWidgets: {},
+		widgetSelectors: [],
+		preview: null,
+		l10n: {
+			widgetTooltip: ''
+		}
+	};
 
 	/**
-	 * wp.customize.WidgetCustomizerPreview
+	 * Init widgets preview.
 	 *
+	 * @since 4.5.0
 	 */
-	api.WidgetCustomizerPreview = {
-		renderedSidebars: {}, // @todo Make rendered a property of the Backbone model
-		renderedWidgets: {}, // @todo Make rendered a property of the Backbone model
-		registeredSidebars: [], // @todo Make a Backbone collection
-		registeredWidgets: {}, // @todo Make array, Backbone collection
-		widgetSelectors: [],
-		preview: null,
-		l10n: {},
+	self.init = function() {
+		var self = this;
 
-		init: function () {
-			var self = this;
+		self.preview = api.preview;
+		if ( api.selectiveRefresh ) {
+			self.addPartials();
+		}
 
-			this.preview = api.preview;
-			this.buildWidgetSelectors();
-			this.highlightControls();
+		self.buildWidgetSelectors();
+		self.highlightControls();
 
-			this.preview.bind( 'highlight-widget', self.highlightWidget );
-		},
+		self.preview.bind( 'highlight-widget', self.highlightWidget );
+
+		api.preview.bind( 'active', function() {
+			self.highlightControls();
+		} );
+	};
+
+	if ( api.selectiveRefresh ) {
 
 		/**
-		 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info
+		 * Partial representing a widget instance.
+		 *
+		 * @class
+		 * @augments wp.customize.selectiveRefresh.Partial
+		 * @since 4.5.0
 		 */
-		buildWidgetSelectors: function () {
-			var self = this;
-
-			$.each( this.registeredSidebars, function ( i, sidebar ) {
-				var widgetTpl = [
-						sidebar.before_widget.replace('%1$s', '').replace('%2$s', ''),
-						sidebar.before_title,
-						sidebar.after_title,
-						sidebar.after_widget
-					].join(''),
-					emptyWidget,
-					widgetSelector,
-					widgetClasses;
-
-				emptyWidget = $(widgetTpl);
-				widgetSelector = emptyWidget.prop('tagName');
-				widgetClasses = emptyWidget.prop('className');
-
-				// Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
-				if ( ! widgetClasses ) {
-					return;
+		self.WidgetPartial = api.selectiveRefresh.Partial.extend({
+
+			/**
+			 * Constructor.
+			 *
+			 * @since 4.5.0
+			 * @param {string} id - Partial ID.
+			 * @param {Object} options
+			 * @param {Object} options.params
+			 */
+			initialize: function( id, options ) {
+				var partial = this, matches;
+				matches = id.match( /^widget\[(.+)]$/ );
+				if ( ! matches ) {
+					throw new Error( 'Illegal id for widget partial.' );
 				}
 
-				widgetClasses = widgetClasses.replace(/^\s+|\s+$/g, '');
+				partial.widgetId = matches[1];
+				options = options || {};
+				options.params = _.extend(
+					{
+						/* Note that a selector of ('#' + partial.widgetId) is faster, but jQuery will only return the one result. */
+						selector: '[id="' + partial.widgetId + '"]', // Alternatively, '[data-customize-widget-id="' + partial.widgetId + '"]'
+						settings: [ self.getWidgetSettingId( partial.widgetId ) ],
+						containerInclusive: true
+					},
+					options.params || {}
+				);
+
+				api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
+			},
 
-				if ( widgetClasses ) {
-					widgetSelector += '.' + widgetClasses.split(/\s+/).join('.');
+			/**
+			 * Send widget-updated message to parent so spinner will get removed from widget control.
+			 *
+			 * @inheritdoc
+			 * @param {wp.customize.selectiveRefresh.Placement} placement
+			 */
+			renderContent: function( placement ) {
+				var partial = this;
+				if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
+					api.preview.send( 'widget-updated', partial.widgetId );
+					api.selectiveRefresh.trigger( 'widget-updated', partial );
 				}
-				self.widgetSelectors.push(widgetSelector);
-			});
-		},
+			}
+		});
 
 		/**
-		 * Highlight the widget on widget updates or widget control mouse overs.
+		 * Partial representing a widget area.
 		 *
-		 * @param  {string} widgetId ID of the widget.
+		 * @class
+		 * @augments wp.customize.selectiveRefresh.Partial
+		 * @since 4.5.0
 		 */
-		highlightWidget: function( widgetId ) {
-			var $body = $( document.body ),
-				$widget = $( '#' + widgetId );
+		self.SidebarPartial = api.selectiveRefresh.Partial.extend({
 
-			$body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
+			/**
+			 * Constructor.
+			 *
+			 * @since 4.5.0
+			 * @param {string} id - Partial ID.
+			 * @param {Object} options
+			 * @param {Object} options.params
+			 */
+			initialize: function( id, options ) {
+				var partial = this, matches;
+				matches = id.match( /^sidebar\[(.+)]$/ );
+				if ( ! matches ) {
+					throw new Error( 'Illegal id for sidebar partial.' );
+				}
+				partial.sidebarId = matches[1];
 
-			$widget.addClass( 'widget-customizer-highlighted-widget' );
-			setTimeout( function () {
-				$widget.removeClass( 'widget-customizer-highlighted-widget' );
-			}, 500 );
-		},
+				options = options || {};
+				options.params = _.extend(
+					{
+						settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
+					},
+					options.params || {}
+				);
 
-		/**
-		 * Show a title and highlight widgets on hover. On shift+clicking
-		 * focus the widget control.
-		 */
-		highlightControls: function() {
-			var self = this,
-				selector = this.widgetSelectors.join(',');
+				api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
+
+				if ( ! partial.params.sidebarArgs ) {
+					throw new Error( 'The sidebarArgs param was not provided.' );
+				}
+				if ( partial.params.settings.length > 1 ) {
+					throw new Error( 'Expected SidebarPartial to only have one associated setting' );
+				}
+			},
+
+			/**
+			 * Set up the partial.
+			 *
+			 * @since 4.5.0
+			 */
+			ready: function() {
+				var sidebarPartial = this;
+
+				// Watch for changes to the sidebar_widgets setting.
+				_.each( sidebarPartial.settings(), function( settingId ) {
+					api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
+				} );
+
+				// Trigger an event for this sidebar being updated whenever a widget inside is rendered.
+				api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
+					var isAssignedWidgetPartial = (
+						placement.partial.extended( self.WidgetPartial ) &&
+						( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
+					);
+					if ( isAssignedWidgetPartial ) {
+						api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
+					}
+				} );
+
+				// Make sure that a widget partial has a container in the DOM prior to a refresh.
+				api.bind( 'change', function( widgetSetting ) {
+					var widgetId, parsedId;
+					parsedId = self.parseWidgetSettingId( widgetSetting.id );
+					if ( ! parsedId ) {
+						return;
+					}
+					widgetId = parsedId.idBase;
+					if ( parsedId.number ) {
+						widgetId += '-' + String( parsedId.number );
+					}
+					if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
+						sidebarPartial.ensureWidgetPlacementContainers( widgetId );
+					}
+				} );
+			},
+
+			/**
+			 * Get the before/after boundary nodes for all instances of this sidebar (usually one).
+			 *
+			 * Note that TreeWalker is not implemented in IE8.
+			 *
+			 * @since 4.5.0
+			 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
+			 */
+			findDynamicSidebarBoundaryNodes: function() {
+				var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
+				regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
+				recursiveCommentTraversal = function( childNodes ) {
+					_.each( childNodes, function( node ) {
+						var matches;
+						if ( 8 === node.nodeType ) {
+							matches = node.nodeValue.match( regExp );
+							if ( ! matches || matches[2] !== partial.sidebarId ) {
+								return;
+							}
+							if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
+								boundaryNodes[ matches[3] ] = {
+									before: null,
+									after: null,
+									instanceNumber: parseInt( matches[3], 10 )
+								};
+							}
+							if ( 'dynamic_sidebar_before' === matches[1] ) {
+								boundaryNodes[ matches[3] ].before = node;
+							} else {
+								boundaryNodes[ matches[3] ].after = node;
+							}
+						} else if ( 1 === node.nodeType ) {
+							recursiveCommentTraversal( node.childNodes );
+						}
+					} );
+				};
+
+				recursiveCommentTraversal( document.body.childNodes );
+				return _.values( boundaryNodes );
+			},
+
+			/**
+			 * Get the placements for this partial.
+			 *
+			 * @since 4.5.0
+			 * @returns {Array}
+			 */
+			placements: function() {
+				var partial = this;
+				return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
+					return new api.selectiveRefresh.Placement( {
+						partial: partial,
+						container: null,
+						startNode: boundaryNodes.before,
+						endNode: boundaryNodes.after,
+						context: {
+							instanceNumber: boundaryNodes.instanceNumber
+						}
+					} );
+				} );
+			},
+
+			/**
+			 * Get the list of widget IDs associated with this widget area.
+			 *
+			 * @since 4.5.0
+			 *
+			 * @returns {Array}
+			 */
+			getWidgetIds: function() {
+				var sidebarPartial = this, settingId, widgetIds;
+				settingId = sidebarPartial.settings()[0];
+				if ( ! settingId ) {
+					throw new Error( 'Missing associated setting.' );
+				}
+				if ( ! api.has( settingId ) ) {
+					throw new Error( 'Setting does not exist.' );
+				}
+				widgetIds = api( settingId ).get();
+				if ( ! _.isArray( widgetIds ) ) {
+					throw new Error( 'Expected setting to be array of widget IDs' );
+				}
+				return widgetIds.slice( 0 );
+			},
+
+			/**
+			 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM.
+			 *
+			 * @since 4.5.0
+			 *
+			 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed.
+			 */
+			reflowWidgets: function() {
+				var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
+				widgetIds = sidebarPartial.getWidgetIds();
+				sidebarPlacements = sidebarPartial.placements();
+
+				widgetPartials = {};
+				_.each( widgetIds, function( widgetId ) {
+					var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
+					if ( widgetPartial ) {
+						widgetPartials[ widgetId ] = widgetPartial;
+					}
+				} );
+
+				_.each( sidebarPlacements, function( sidebarPlacement ) {
+					var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
+
+					// Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
+					_.each( widgetPartials, function( widgetPartial ) {
+						_.each( widgetPartial.placements(), function( widgetPlacement ) {
+
+							if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
+								thisPosition = widgetPlacement.container.index();
+								sidebarWidgets.push( {
+									partial: widgetPartial,
+									placement: widgetPlacement,
+									position: thisPosition
+								} );
+								if ( thisPosition < lastPosition ) {
+									needsSort = true;
+								}
+								lastPosition = thisPosition;
+							}
+						} );
+					} );
+
+					if ( needsSort ) {
+						_.each( sidebarWidgets, function( sidebarWidget ) {
+							sidebarPlacement.endNode.parentNode.insertBefore(
+								sidebarWidget.placement.container[0],
+								sidebarPlacement.endNode
+							);
+
+							// @todo Rename partial-placement-moved?
+							api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
+						} );
+
+						sortedSidebarContainers.push( sidebarPlacement );
+					}
+				} );
+
+				if ( sortedSidebarContainers.length > 0 ) {
+					api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
+				}
+
+				return sortedSidebarContainers;
+			},
+
+			/**
+			 * Make sure there is a widget instance container in this sidebar for the given widget ID.
+			 *
+			 * @since 4.5.0
+			 *
+			 * @param {string} widgetId
+			 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial.
+			 */
+			ensureWidgetPlacementContainers: function( widgetId ) {
+				var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
+				widgetPartial = api.selectiveRefresh.partial( partialId );
+				if ( ! widgetPartial ) {
+					widgetPartial = new self.WidgetPartial( partialId, {
+						params: {}
+					} );
+					api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
+				}
 
-			$(selector).attr( 'title', this.l10n.widgetTooltip );
+				// Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
+				_.each( sidebarPartial.placements(), function( sidebarPlacement ) {
+					var foundWidgetPlacement, widgetContainerElement;
 
-			$(document).on( 'mouseenter', selector, function () {
-				self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
-			});
+					foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
+						return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
+					} );
+					if ( foundWidgetPlacement ) {
+						return;
+					}
 
-			// Open expand the widget control when shift+clicking the widget element
-			$(document).on( 'click', selector, function ( e ) {
-				if ( ! e.shiftKey ) {
+					widgetContainerElement = $(
+						sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) +
+						sidebarPartial.params.sidebarArgs.after_widget
+					);
+
+					widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
+					widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
+					widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
+
+					/*
+					 * Make sure the widget container element has the customize-container context data.
+					 * The sidebar_instance_number is used to disambiguate multiple instances of the
+					 * same sidebar are rendered onto the template, and so the same widget is embedded
+					 * multiple times.
+					 */
+					widgetContainerElement.data( 'customize-partial-placement-context', {
+						'sidebar_id': sidebarPartial.sidebarId,
+						'sidebar_instance_number': sidebarPlacement.context.instanceNumber
+					} );
+
+					sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
+					wasInserted = true;
+				} );
+
+				if ( wasInserted ) {
+					sidebarPartial.reflowWidgets();
+				}
+
+				return widgetPartial;
+			},
+
+			/**
+			 * Handle change to the sidebars_widgets[] setting.
+			 *
+			 * @since 4.5.0
+			 *
+			 * @param {Array} newWidgetIds New widget ids.
+			 * @param {Array} oldWidgetIds Old widget ids.
+			 */
+			handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
+				var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
+
+				needsRefresh = (
+					( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
+					( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
+				);
+				if ( needsRefresh ) {
+					sidebarPartial.fallback();
 					return;
 				}
-				e.preventDefault();
 
-				self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
-			});
+				// Handle removal of widgets.
+				widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
+				_.each( widgetsRemoved, function( removedWidgetId ) {
+					var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
+					if ( widgetPartial ) {
+						_.each( widgetPartial.placements(), function( placement ) {
+							var isRemoved = (
+								placement.context.sidebar_id === sidebarPartial.sidebarId ||
+								( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
+							);
+							if ( isRemoved ) {
+								placement.container.remove();
+							}
+						} );
+					}
+				} );
+
+				// Handle insertion of widgets.
+				widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
+				_.each( widgetsAdded, function( addedWidgetId ) {
+					var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
+					addedWidgetPartials.push( widgetPartial );
+				} );
+
+				_.each( addedWidgetPartials, function( widgetPartial ) {
+					widgetPartial.refresh();
+				} );
+
+				api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
+			},
+
+			/**
+			 * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
+			 *
+			 * @since 4.5.0
+			 */
+			refresh: function() {
+				var partial = this, deferred = $.Deferred();
+
+				deferred.fail( function() {
+					partial.fallback();
+				} );
+
+				if ( 0 === partial.placements().length ) {
+					deferred.reject();
+				} else {
+					_.each( partial.reflowWidgets(), function( sidebarPlacement ) {
+						api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
+					} );
+					deferred.resolve();
+				}
+
+				return deferred.promise();
+			}
+		});
+
+		api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
+		api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
+
+		/**
+		 * Add partials for the registered widget areas (sidebars).
+		 *
+		 * @since 4.5.0
+		 */
+		self.addPartials = function() {
+			_.each( self.registeredSidebars, function( registeredSidebar ) {
+				var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
+				partial = api.selectiveRefresh.partial( partialId );
+				if ( ! partial ) {
+					partial = new self.SidebarPartial( partialId, {
+						params: {
+							sidebarArgs: registeredSidebar
+						}
+					} );
+					api.selectiveRefresh.partial.add( partial.id, partial );
+				}
+			} );
+		};
+
+	}
+
+	/**
+	 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
+	 *
+	 * @since 3.9.0
+	 */
+	self.buildWidgetSelectors = function() {
+		var self = this;
+
+		$.each( self.registeredSidebars, function( i, sidebar ) {
+			var widgetTpl = [
+					sidebar.before_widget.replace( '%1$s', '' ).replace( '%2$s', '' ),
+					sidebar.before_title,
+					sidebar.after_title,
+					sidebar.after_widget
+				].join( '' ),
+				emptyWidget,
+				widgetSelector,
+				widgetClasses;
+
+			emptyWidget = $( widgetTpl );
+			widgetSelector = emptyWidget.prop( 'tagName' );
+			widgetClasses = emptyWidget.prop( 'className' );
+
+			// Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
+			if ( ! widgetClasses ) {
+				return;
+			}
+
+			widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );
+
+			if ( widgetClasses ) {
+				widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
+			}
+			self.widgetSelectors.push( widgetSelector );
+		});
+	};
+
+	/**
+	 * Highlight the widget on widget updates or widget control mouse overs.
+	 *
+	 * @since 3.9.0
+	 * @param  {string} widgetId ID of the widget.
+	 */
+	self.highlightWidget = function( widgetId ) {
+		var $body = $( document.body ),
+			$widget = $( '#' + widgetId );
+
+		$body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
+
+		$widget.addClass( 'widget-customizer-highlighted-widget' );
+		setTimeout( function() {
+			$widget.removeClass( 'widget-customizer-highlighted-widget' );
+		}, 500 );
+	};
+
+	/**
+	 * Show a title and highlight widgets on hover. On shift+clicking
+	 * focus the widget control.
+	 *
+	 * @since 3.9.0
+	 */
+	self.highlightControls = function() {
+		var self = this,
+			selector = this.widgetSelectors.join( ',' );
+
+		$( selector ).attr( 'title', this.l10n.widgetTooltip );
+
+		$( document ).on( 'mouseenter', selector, function() {
+			self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
+		});
+
+		// Open expand the widget control when shift+clicking the widget element
+		$( document ).on( 'click', selector, function( e ) {
+			if ( ! e.shiftKey ) {
+				return;
+			}
+			e.preventDefault();
+
+			self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
+		});
+	};
+
+	/**
+	 * Parse a widget ID.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param {string} widgetId Widget ID.
+	 * @returns {{idBase: string, number: number|null}}
+	 */
+	self.parseWidgetId = function( widgetId ) {
+		var matches, parsed = {
+			idBase: '',
+			number: null
+		};
+
+		matches = widgetId.match( /^(.+)-(\d+)$/ );
+		if ( matches ) {
+			parsed.idBase = matches[1];
+			parsed.number = parseInt( matches[2], 10 );
+		} else {
+			parsed.idBase = widgetId; // Likely an old single widget.
+		}
+
+		return parsed;
+	};
+
+	/**
+	 * Parse a widget setting ID.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param {string} settingId Widget setting ID.
+	 * @returns {{idBase: string, number: number|null}|null}
+	 */
+	self.parseWidgetSettingId = function( settingId ) {
+		var matches, parsed = {
+			idBase: '',
+			number: null
+		};
+
+		matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
+		if ( ! matches ) {
+			return null;
 		}
+		parsed.idBase = matches[1];
+		if ( matches[2] ) {
+			parsed.number = parseInt( matches[2], 10 );
+		}
+		return parsed;
 	};
 
-	$(function () {
-		var settings = window._wpWidgetCustomizerPreviewSettings;
-		if ( ! settings ) {
-			return;
+	/**
+	 * Convert a widget ID into a Customizer setting ID.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param {string} widgetId Widget ID.
+	 * @returns {string} settingId Setting ID.
+	 */
+	self.getWidgetSettingId = function( widgetId ) {
+		var parsed = this.parseWidgetId( widgetId ), settingId;
+
+		settingId = 'widget_' + parsed.idBase;
+		if ( parsed.number ) {
+			settingId += '[' + String( parsed.number ) + ']';
 		}
 
-		$.extend( api.WidgetCustomizerPreview, settings );
+		return settingId;
+	};
 
-		api.WidgetCustomizerPreview.init();
+	api.bind( 'preview-ready', function() {
+		$.extend( self, _wpWidgetCustomizerPreviewSettings );
+		self.init();
 	});
 
-})( window.wp, jQuery );
+	return self;
+})( jQuery, _, wp, wp.customize );
diff --git src/wp-includes/js/customize-selective-refresh.js src/wp-includes/js/customize-selective-refresh.js
new file mode 100644
index 0000000..3da6144
--- /dev/null
+++ src/wp-includes/js/customize-selective-refresh.js
@@ -0,0 +1,849 @@
+/* global jQuery, JSON, _customizePartialRefreshExports, console */
+
+wp.customize.selectiveRefresh = ( function( $, api ) {
+	'use strict';
+	var self, Partial, Placement;
+
+	self = {
+		ready: $.Deferred(),
+		data: {
+			partials: {},
+			renderQueryVar: '',
+			l10n: {
+				shiftClickToEdit: ''
+			},
+			refreshBuffer: 250
+		},
+		currentRequest: null
+	};
+
+	_.extend( self, api.Events );
+
+	/**
+	 * A Customizer Partial.
+	 *
+	 * A partial provides a rendering of one or more settings according to a template.
+	 *
+	 * @see PHP class WP_Customize_Partial.
+	 *
+	 * @class
+	 * @augments wp.customize.Class
+	 * @since 4.5.0
+	 *
+	 * @param {string} id                              Unique identifier for the control instance.
+	 * @param {object} options                         Options hash for the control instance.
+	 * @param {object} options.params
+	 * @param {string} options.params.type             Type of partial (e.g. nav_menu, widget, etc)
+	 * @param {string} options.params.selector         jQuery selector to find the container element in the page.
+	 * @param {array}  options.params.settings         The IDs for the settings the partial relates to.
+	 * @param {string} options.params.primarySetting   The ID for the primary setting the partial renders.
+	 * @param {bool}   options.params.fallbackRefresh  Whether to refresh the entire preview in case of a partial refresh failure.
+	 */
+	Partial = self.Partial = api.Class.extend({
+
+		id: null,
+
+		 /**
+		 * Constructor.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param {string} id - Partial ID.
+		 * @param {Object} options
+		 * @param {Object} options.params
+		 */
+		initialize: function( id, options ) {
+			var partial = this;
+			options = options || {};
+			partial.id = id;
+
+			partial.params = _.extend(
+				{
+					selector: null,
+					settings: [],
+					primarySetting: null,
+					containerInclusive: false,
+					fallbackRefresh: true // Note this needs to be false in a frontend editing context.
+				},
+				options.params || {}
+			);
+
+			partial.deferred = {};
+			partial.deferred.ready = $.Deferred();
+
+			partial.deferred.ready.done( function() {
+				partial.ready();
+			} );
+		},
+
+		/**
+		 * Set up the partial.
+		 *
+		 * @since 4.5.0
+		 */
+		ready: function() {
+			var partial = this;
+			_.each( _.pluck( partial.placements(), 'container' ), function( container ) {
+				$( container ).attr( 'title', self.data.l10n.shiftClickToEdit );
+			} );
+			$( document ).on( 'click', partial.params.selector, function( e ) {
+				if ( ! e.shiftKey ) {
+					return;
+				}
+				e.preventDefault();
+				_.each( partial.placements(), function( placement ) {
+					if ( $( placement.container ).is( e.currentTarget ) ) {
+						partial.showControl();
+					}
+				} );
+			} );
+		},
+
+		/**
+		 * Find all placements for this partial int he document.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @return {Array.<Placement>}
+		 */
+		placements: function() {
+			var partial = this, selector;
+
+			selector = partial.params.selector;
+			if ( selector ) {
+				selector += ', ';
+			}
+			selector += '[data-customize-partial-id="' + partial.id + '"]'; // @todo Consider injecting customize-partial-id-${id} classnames instead.
+
+			return $( selector ).map( function() {
+				var container = $( this ), context;
+
+				context = container.data( 'customize-partial-placement-context' );
+				if ( _.isString( context ) && '{' === context.substr( 0, 1 ) ) {
+					throw new Error( 'context JSON parse error' );
+				}
+
+				return new Placement( {
+					partial: partial,
+					container: container,
+					context: context
+				} );
+			} ).get();
+		},
+
+		/**
+		 * Get list of setting IDs related to this partial.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @return {String[]}
+		 */
+		settings: function() {
+			var partial = this;
+			if ( partial.params.settings && 0 !== partial.params.settings.length ) {
+				return partial.params.settings;
+			} else if ( partial.params.primarySetting ) {
+				return [ partial.params.primarySetting ];
+			} else {
+				return [ partial.id ];
+			}
+		},
+
+		/**
+		 * Return whether the setting is related to the partial.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param {wp.customize.Value|string} setting  ID or object for setting.
+		 * @return {boolean} Whether the setting is related to the partial.
+		 */
+		isRelatedSetting: function( setting /*... newValue, oldValue */ ) {
+			var partial = this;
+			if ( _.isString( setting ) ) {
+				setting = api( setting );
+			}
+			if ( ! setting ) {
+				return false;
+			}
+			return -1 !== _.indexOf( partial.settings(), setting.id );
+		},
+
+		/**
+		 * Show the control to modify this partial's setting(s).
+		 *
+		 * This may be overridden for inline editing.
+		 *
+		 * @since 4.5.0
+		 */
+		showControl: function() {
+			var partial = this, settingId = partial.params.primarySetting;
+			if ( ! settingId ) {
+				settingId = _.first( partial.settings() );
+			}
+			api.preview.send( 'focus-control-for-setting', settingId );
+		},
+
+		/**
+		 * Prepare container for selective refresh.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param {Placement} placement
+		 */
+		preparePlacement: function( placement ) {
+			$( placement.container ).addClass( 'customize-partial-refreshing' );
+		},
+
+		/**
+		 * Reference to the pending promise returned from self.requestPartial().
+		 *
+		 * @since 4.5.0
+		 * @private
+		 */
+		_pendingRefreshPromise: null,
+
+		/**
+		 * Request the new partial and render it into the placements.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @this {wp.customize.selectiveRefresh.Partial}
+		 * @return {jQuery.Promise}
+		 */
+		refresh: function() {
+			var partial = this, refreshPromise;
+
+			refreshPromise = self.requestPartial( partial );
+
+			if ( ! partial._pendingRefreshPromise ) {
+				_.each( partial.placements(), function( placement ) {
+					partial.preparePlacement( placement );
+				} );
+
+				refreshPromise.done( function( placements ) {
+					_.each( placements, function( placement ) {
+						partial.renderContent( placement );
+					} );
+				} );
+
+				refreshPromise.fail( function( data, placements ) {
+					partial.fallback( data, placements );
+				} );
+
+				// Allow new request when this one finishes.
+				partial._pendingRefreshPromise = refreshPromise;
+				refreshPromise.always( function() {
+					partial._pendingRefreshPromise = null;
+				} );
+			}
+
+			return refreshPromise;
+		},
+
+		/**
+		 * Apply the addedContent in the placement to the document.
+		 *
+		 * Note the placement object will have its container and removedNodes
+		 * properties updated.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param {Placement}             placement
+		 * @param {Element|jQuery}        [placement.container]  - This param will be empty if there was no element matching the selector.
+		 * @param {string|object|boolean} placement.addedContent - Rendered HTML content, a data object for JS templates to render, or false if no render.
+		 * @param {object}                [placement.context]    - Optional context information about the container.
+		 * @returns {boolean} Whether the rendering was successful and the fallback was not invoked.
+		 */
+		renderContent: function( placement ) {
+			var partial = this, content, newContainerElement;
+			if ( ! placement.container ) {
+				partial.fallback( new Error( 'no_container' ), [ placement ] );
+				return false;
+			}
+			placement.container = $( placement.container );
+			if ( false === placement.addedContent ) {
+				partial.fallback( new Error( 'missing_render' ), [ placement ] );
+				return false;
+			}
+
+			// Currently a subclass needs to override renderContent to handle partials returning data object.
+			if ( ! _.isString( placement.addedContent ) ) {
+				partial.fallback( new Error( 'non_string_content' ), [ placement ] );
+				return false;
+			}
+
+			content = placement.addedContent;
+			if ( wp.emoji && wp.emoji.parse && ! $.contains( document.head, placement.container[0] ) ) {
+				content = wp.emoji.parse( content );
+			}
+
+			// @todo Should containerInclusive be context information as opposed to a param?
+			if ( partial.params.containerInclusive ) {
+
+				// Note that content may be an empty string, and in this case jQuery will just remove the oldContainer
+				newContainerElement = $( content );
+
+				// Merge the new context on top of the old context.
+				placement.context = _.extend(
+					placement.context,
+					newContainerElement.data( 'customize-partial-placement-context' ) || {}
+				);
+				newContainerElement.data( 'customize-partial-placement-context', placement.context );
+
+				placement.removedNodes = placement.container;
+				placement.container = newContainerElement;
+				placement.removedNodes.replaceWith( placement.container );
+				placement.container.attr( 'title', self.data.l10n.shiftClickToEdit );
+			} else {
+				placement.removedNodes = document.createDocumentFragment();
+				while ( placement.container[0].firstChild ) {
+					placement.removedNodes.appendChild( placement.container[0].firstChild );
+				}
+
+				placement.container.html( content );
+			}
+
+			placement.container.removeClass( 'customize-partial-refreshing' );
+
+			// Prevent placement container from being being re-triggered as being rendered among nested partials.
+			placement.container.data( 'customize-partial-content-rendered', true );
+
+			/**
+			 * Announce when a partial's placement has been rendered so that dynamic elements can be re-built.
+			 */
+			self.trigger( 'partial-content-rendered', placement );
+			return true;
+		},
+
+		/**
+		 * Handle fail to render partial.
+		 *
+		 * The first argument is either the failing jqXHR or an Error object, and the second argument is the array of containers.
+		 *
+		 * @since 4.5.0
+		 */
+		fallback: function() {
+			var partial = this;
+			if ( partial.params.fallbackRefresh ) {
+				self.requestFullRefresh();
+			}
+		}
+	} );
+
+	/**
+	 * A Placement for a Partial.
+	 *
+	 * A partial placement is the actual physical representation of a partial for a given context.
+	 * It also may have information in relation to how a placement may have just changed.
+	 * The placement is conceptually similar to a DOM Range or MutationRecord.
+	 *
+	 * @class
+	 * @augments wp.customize.Class
+	 * @since 4.5.0
+	 */
+	self.Placement = Placement = api.Class.extend({
+
+		/**
+		 * The partial with which the container is associated.
+		 *
+		 * @param {wp.customize.selectiveRefresh.Partial}
+		 */
+		partial: null,
+
+		/**
+		 * DOM element which contains the placement's contents.
+		 *
+		 * This will be null if the startNode and endNode do not point to the same
+		 * DOM element, such as in the case of a sidebar partial.
+		 * This container element itself will be replaced for partials that
+		 * have containerInclusive param defined as true.
+		 */
+		container: null,
+
+		/**
+		 * DOM node for the initial boundary of the placement.
+		 *
+		 * This will normally be the same as endNode since most placements appear as elements.
+		 * This is primarily useful for widget sidebars which do not have intrinsic containers, but
+		 * for which an HTML comment is output before to mark the starting position.
+		 */
+		startNode: null,
+
+		/**
+		 * DOM node for the terminal boundary of the placement.
+		 *
+		 * This will normally be the same as startNode since most placements appear as elements.
+		 * This is primarily useful for widget sidebars which do not have intrinsic containers, but
+		 * for which an HTML comment is output before to mark the ending position.
+		 */
+		endNode: null,
+
+		/**
+		 * Context data.
+		 *
+		 * This provides information about the placement which is included in the request
+		 * in order to render the partial properly.
+		 *
+		 * @param {object}
+		 */
+		context: null,
+
+		/**
+		 * The content for the partial when refreshed.
+		 *
+		 * @param {string}
+		 */
+		addedContent: null,
+
+		/**
+		 * DOM node(s) removed when the partial is refreshed.
+		 *
+		 * If the partial is containerInclusive, then the removedNodes will be
+		 * the single Element that was the partial's former placement. If the
+		 * partial is not containerInclusive, then the removedNodes will be a
+		 * documentFragment containing the nodes removed.
+		 *
+		 * @param {Element|DocumentFragment}
+		 */
+		removedNodes: null,
+
+		/**
+		 * Constructor.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param {object}                   args
+		 * @param {Partial}                  args.partial
+		 * @param {jQuery|Element}           [args.container]
+		 * @param {Node}                     [args.startNode]
+		 * @param {Node}                     [args.endNode]
+		 * @param {object}                   [args.context]
+		 * @param {string}                   [args.addedContent]
+		 * @param {jQuery|DocumentFragment}  [args.removedNodes]
+		 */
+		initialize: function( args ) {
+			var placement = this;
+
+			args = _.extend( {}, args || {} );
+			if ( ! args.partial || ! args.partial.extended( Partial ) ) {
+				throw new Error( 'Missing partial' );
+			}
+			args.context = args.context || {};
+			if ( args.container ) {
+				args.container = $( args.container );
+			}
+
+			_.extend( placement, args );
+		}
+
+	});
+
+	/**
+	 * Mapping of type names to Partial constructor subclasses.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @type {Object.<string, wp.customize.selectiveRefresh.Partial>}
+	 */
+	self.partialConstructor = {};
+
+	self.partial = new api.Values({ defaultConstructor: Partial });
+
+	/**
+	 * Get the POST vars for a Customizer preview request.
+	 *
+	 * @since 4.5.0
+	 * @see wp.customize.previewer.query()
+	 *
+	 * @return {object}
+	 */
+	self.getCustomizeQuery = function() {
+		var dirtyCustomized = {};
+		api.each( function( value, key ) {
+			if ( value._dirty ) {
+				dirtyCustomized[ key ] = value();
+			}
+		} );
+
+		return {
+			wp_customize: 'on',
+			nonce: api.settings.nonce.preview,
+			theme: api.settings.theme.stylesheet,
+			customized: JSON.stringify( dirtyCustomized )
+		};
+	};
+
+	/**
+	 * Currently-requested partials and their associated deferreds.
+	 *
+	 * @since 4.5.0
+	 * @type {Object<string, { deferred: jQuery.Promise, partial: wp.customize.selectiveRefresh.Partial }>}
+	 */
+	self._pendingPartialRequests = {};
+
+	/**
+	 * Timeout ID for the current requesr, or null if no request is current.
+	 *
+	 * @since 4.5.0
+	 * @type {number|null}
+	 * @private
+	 */
+	self._debouncedTimeoutId = null;
+
+	/**
+	 * Current jqXHR for the request to the partials.
+	 *
+	 * @since 4.5.0
+	 * @type {jQuery.jqXHR|null}
+	 * @private
+	 */
+	self._currentRequest = null;
+
+	/**
+	 * Request full page refresh.
+	 *
+	 * When selective refresh is embedded in the context of frontend editing, this request
+	 * must fail or else changes will be lost, unless transactions are implemented.
+	 *
+	 * @since 4.5.0
+	 */
+	self.requestFullRefresh = function() {
+		api.preview.send( 'refresh' );
+	};
+
+	/**
+	 * Request a re-rendering of a partial.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param {wp.customize.selectiveRefresh.Partial} partial
+	 * @return {jQuery.Promise}
+	 */
+	self.requestPartial = function( partial ) {
+		var partialRequest;
+
+		if ( self._debouncedTimeoutId ) {
+			clearTimeout( self._debouncedTimeoutId );
+			self._debouncedTimeoutId = null;
+		}
+		if ( self._currentRequest ) {
+			self._currentRequest.abort();
+			self._currentRequest = null;
+		}
+
+		partialRequest = self._pendingPartialRequests[ partial.id ];
+		if ( ! partialRequest || 'pending' !== partialRequest.deferred.state() ) {
+			partialRequest = {
+				deferred: $.Deferred(),
+				partial: partial
+			};
+			self._pendingPartialRequests[ partial.id ] = partialRequest;
+		}
+
+		// Prevent leaking partial into debounced timeout callback.
+		partial = null;
+
+		self._debouncedTimeoutId = setTimeout(
+			function() {
+				var data, partialPlacementContexts, partialsPlacements, request;
+
+				self._debouncedTimeoutId = null;
+				data = self.getCustomizeQuery();
+
+				/*
+				 * It is key that the containers be fetched exactly at the point of the request being
+				 * made, because the containers need to be mapped to responses by array indices.
+				 */
+				partialsPlacements = {};
+
+				partialPlacementContexts = {};
+
+				_.each( self._pendingPartialRequests, function( pending, partialId ) {
+					partialsPlacements[ partialId ] = pending.partial.placements();
+					if ( ! self.partial.has( partialId ) ) {
+						pending.deferred.rejectWith( pending.partial, [ new Error( 'partial_removed' ), partialsPlacements[ partialId ] ] );
+					} else {
+						/*
+						 * Note that this may in fact be an empty array. In that case, it is the responsibility
+						 * of the Partial subclass instance to know where to inject the response, or else to
+						 * just issue a refresh (default behavior). The data being returned with each container
+						 * is the context information that may be needed to render certain partials, such as
+						 * the contained sidebar for rendering widgets or what the nav menu args are for a menu.
+						 */
+						partialPlacementContexts[ partialId ] = _.map( partialsPlacements[ partialId ], function( placement ) {
+							return placement.context || {};
+						} );
+					}
+				} );
+
+				data.partials = JSON.stringify( partialPlacementContexts );
+				data[ self.data.renderQueryVar ] = '1';
+
+				request = self._currentRequest = wp.ajax.send( null, {
+					data: data,
+					url: api.settings.url.self
+				} );
+
+				request.done( function( data ) {
+
+					/**
+					 * Announce the data returned from a request to render partials.
+					 *
+					 * The data is filtered on the server via customize_render_partials_response
+					 * so plugins can inject data from the server to be utilized
+					 * on the client via this event. Plugins may use this filter
+					 * to communicate script and style dependencies that need to get
+					 * injected into the page to support the rendered partials.
+					 * This is similar to the 'saved' event.
+					 */
+					self.trigger( 'render-partials-response', data );
+
+					// Relay errors (warnings) captured during rendering and relay to console.
+					if ( data.errors && 'undefined' !== typeof console && console.warn ) {
+						_.each( data.errors, function( error ) {
+							console.warn( error );
+						} );
+					}
+
+					/*
+					 * Note that data is an array of items that correspond to the array of
+					 * containers that were submitted in the request. So we zip up the
+					 * array of containers with the array of contents for those containers,
+					 * and send them into .
+					 */
+					_.each( self._pendingPartialRequests, function( pending, partialId ) {
+						var placementsContents;
+						if ( ! _.isArray( data.contents[ partialId ] ) ) {
+							pending.deferred.rejectWith( pending.partial, [ new Error( 'unrecognized_partial' ), partialsPlacements[ partialId ] ] );
+						} else {
+							placementsContents = _.map( data.contents[ partialId ], function( content, i ) {
+								var partialPlacement = partialsPlacements[ partialId ][ i ];
+								if ( partialPlacement ) {
+									partialPlacement.addedContent = content;
+								} else {
+									partialPlacement = new Placement( {
+										partial: pending.partial,
+										addedContent: content
+									} );
+								}
+								return partialPlacement;
+							} );
+							pending.deferred.resolveWith( pending.partial, [ placementsContents ] );
+						}
+					} );
+					self._pendingPartialRequests = {};
+				} );
+
+				request.fail( function( data, statusText ) {
+
+					/*
+					 * Ignore failures caused by partial.currentRequest.abort()
+					 * The pending deferreds will remain in self._pendingPartialRequests
+					 * for re-use with the next request.
+					 */
+					if ( 'abort' === statusText ) {
+						return;
+					}
+
+					_.each( self._pendingPartialRequests, function( pending, partialId ) {
+						pending.deferred.rejectWith( pending.partial, [ data, partialsPlacements[ partialId ] ] );
+					} );
+					self._pendingPartialRequests = {};
+				} );
+			},
+			self.data.refreshBuffer
+		);
+
+		return partialRequest.deferred.promise();
+	};
+
+	/**
+	 * Add partials for any nav menu container elements in the document.
+	 *
+	 * This method may be called multiple times. Containers that already have been
+	 * seen will be skipped.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param {jQuery|HTMLElement} [rootElement]
+	 * @param {object}             [options]
+	 * @param {boolean=true}       [options.triggerRendered]
+	 */
+	self.addPartials = function( rootElement, options ) {
+		var containerElements;
+		if ( ! rootElement ) {
+			rootElement = document.documentElement;
+		}
+		rootElement = $( rootElement );
+		options = _.extend(
+			{
+				triggerRendered: true
+			},
+			options || {}
+		);
+
+		containerElements = rootElement.find( '[data-customize-partial-id]' );
+		if ( rootElement.is( '[data-customize-partial-id]' ) ) {
+			containerElements = containerElements.add( rootElement );
+		}
+		containerElements.each( function() {
+			var containerElement = $( this ), partial, id, Constructor, partialOptions, containerContext;
+			id = containerElement.data( 'customize-partial-id' );
+			if ( ! id ) {
+				return;
+			}
+			containerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
+
+			partial = self.partial( id );
+			if ( ! partial ) {
+				partialOptions = containerElement.data( 'customize-partial-options' ) || {};
+				partialOptions.constructingContainerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
+				Constructor = self.partialConstructor[ containerElement.data( 'customize-partial-type' ) ] || self.Partial;
+				partial = new Constructor( id, partialOptions );
+				self.partial.add( partial.id, partial );
+			}
+
+			/*
+			 * Only trigger renders on (nested) partials that have been not been
+			 * handled yet. An example where this would apply is a nav menu
+			 * embedded inside of a custom menu widget. When the widget's title
+			 * is updated, the entire widget will re-render and then the event
+			 * will be triggered for the nested nav menu to do any initialization.
+			 */
+			if ( options.triggerRendered && ! containerElement.data( 'customize-partial-content-rendered' ) ) {
+
+				/**
+				 * Announce when a partial's nested placement has been re-rendered.
+				 */
+				self.trigger( 'partial-content-rendered', new Placement( {
+					partial: partial,
+					context: containerContext,
+					container: containerElement
+				} ) );
+			}
+			containerElement.data( 'customize-partial-content-rendered', true );
+		} );
+	};
+
+	api.bind( 'preview-ready', function() {
+		var handleSettingChange, watchSettingChange, unwatchSettingChange;
+
+		// Polyfill for IE8 to support the document.head attribute.
+		if ( ! document.head ) {
+			document.head = $( 'head:first' )[0];
+		}
+
+		_.extend( self.data, _customizePartialRefreshExports );
+
+		// Create the partial JS models.
+		_.each( self.data.partials, function( data, id ) {
+			var Constructor, partial = self.partial( id );
+			if ( ! partial ) {
+				Constructor = self.partialConstructor[ data.type ] || self.Partial;
+				partial = new Constructor( id, { params: data } );
+				self.partial.add( id, partial );
+			} else {
+				_.extend( partial.params, data );
+			}
+		} );
+
+		/**
+		 * Handle change to a setting.
+		 *
+		 * Note this is largely needed because adding a 'change' event handler to wp.customize
+		 * will only include the changed setting object as an argument, not including the
+		 * new value or the old value.
+		 *
+		 * @since 4.5.0
+		 * @this {wp.customize.Setting}
+		 *
+		 * @param {*|null} newValue New value, or null if the setting was just removed.
+		 * @param {*|null} oldValue Old value, or null if the setting was just added.
+		 */
+		handleSettingChange = function( newValue, oldValue ) {
+			var setting = this;
+			self.partial.each( function( partial ) {
+				if ( partial.isRelatedSetting( setting, newValue, oldValue ) ) {
+					partial.refresh();
+				}
+			} );
+		};
+
+		/**
+		 * Trigger the initial change for the added setting, and watch for changes.
+		 *
+		 * @since 4.5.0
+		 * @this {wp.customize.Values}
+		 *
+		 * @param {wp.customize.Setting} setting
+		 */
+		watchSettingChange = function( setting ) {
+			handleSettingChange.call( setting, setting(), null );
+			setting.bind( handleSettingChange );
+		};
+
+		/**
+		 * Trigger the final change for the removed setting, and unwatch for changes.
+		 *
+		 * @since 4.5.0
+		 * @this {wp.customize.Values}
+		 *
+		 * @param {wp.customize.Setting} setting
+		 */
+		unwatchSettingChange = function( setting ) {
+			handleSettingChange.call( setting, null, setting() );
+			setting.unbind( handleSettingChange );
+		};
+
+		api.bind( 'add', watchSettingChange );
+		api.bind( 'remove', unwatchSettingChange );
+		api.each( function( setting ) {
+			setting.bind( handleSettingChange );
+		} );
+
+		// Add (dynamic) initial partials that are declared via data-* attributes.
+		self.addPartials( document.documentElement, {
+			triggerRendered: false
+		} );
+
+		// Add new dynamic partials when the document changes.
+		if ( 'undefined' !== typeof MutationObserver ) {
+			self.mutationObserver = new MutationObserver( function( mutations ) {
+				_.each( mutations, function( mutation ) {
+					self.addPartials( $( mutation.target ) );
+				} );
+			} );
+			self.mutationObserver.observe( document.documentElement, {
+				childList: true,
+				subtree: true
+			} );
+		}
+
+		/**
+		 * Handle rendering of partials.
+		 *
+		 * @param {api.selectiveRefresh.Placement} placement
+		 */
+		api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
+			if ( placement.container ) {
+				self.addPartials( placement.container );
+			}
+		} );
+
+		api.preview.bind( 'active', function() {
+
+			// Make all partials ready.
+			self.partial.each( function( partial ) {
+				partial.deferred.ready.resolve();
+			} );
+
+			// Make all partials added henceforth as ready upon add.
+			self.partial.bind( 'add', function( partial ) {
+				partial.deferred.ready.resolve();
+			} );
+		} );
+
+	} );
+
+	return self;
+}( jQuery, wp.customize ) );
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
index 1a9d259..01be0f5 100644
--- src/wp-includes/script-loader.php
+++ src/wp-includes/script-loader.php
@@ -447,6 +447,7 @@ function wp_default_scripts( &$scripts ) {
 		// Used for overriding the file types allowed in plupload.
 		'allowedFiles'       => __( 'Allowed Files' ),
 	) );
+	$scripts->add( 'customize-selective-refresh', "/wp-includes/js/customize-selective-refresh$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
 
 	$scripts->add( 'customize-widgets', "/wp-admin/js/customize-widgets$suffix.js", array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-droppable', 'wp-backbone', 'customize-controls' ), false, 1 );
 	$scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
diff --git tests/phpunit/tests/customize/manager.php tests/phpunit/tests/customize/manager.php
index 0b86b4c..6f5789d 100644
--- tests/phpunit/tests/customize/manager.php
+++ tests/phpunit/tests/customize/manager.php
@@ -425,7 +425,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 		$data = json_decode( $json, true );
 		$this->assertNotEmpty( $data );
 
-		$this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices' ), array_keys( $data ) );
+		$this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'selectiveRefreshEnabled' ), array_keys( $data ) );
 		$this->assertEquals( $autofocus, $data['autofocus'] );
 		$this->assertArrayHasKey( 'save', $data['nonce'] );
 		$this->assertArrayHasKey( 'preview', $data['nonce'] );
diff --git tests/phpunit/tests/customize/nav-menu-item-setting.php tests/phpunit/tests/customize/nav-menu-item-setting.php
index 39ed42e..3431ef8 100644
--- tests/phpunit/tests/customize/nav-menu-item-setting.php
+++ tests/phpunit/tests/customize/nav-menu-item-setting.php
@@ -69,7 +69,6 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase {
 
 		$setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[123]' );
 		$this->assertEquals( 'nav_menu_item', $setting->type );
-		$this->assertEquals( 'postMessage', $setting->transport );
 		$this->assertEquals( 123, $setting->post_id );
 		$this->assertNull( $setting->previous_post_id );
 		$this->assertNull( $setting->update_status );
diff --git tests/phpunit/tests/customize/nav-menus.php tests/phpunit/tests/customize/nav-menus.php
index 2969a2d..a65b428 100644
--- tests/phpunit/tests/customize/nav-menus.php
+++ tests/phpunit/tests/customize/nav-menus.php
@@ -353,11 +353,11 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 
 		$expected = array( 'type' => 'nav_menu_item' );
 		$results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu_item[123]' );
-		$this->assertEquals( $expected, $results );
+		$this->assertEquals( $expected['type'], $results['type'] );
 
 		$expected = array( 'type' => 'nav_menu' );
 		$results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu[123]' );
-		$this->assertEquals( $expected, $results );
+		$this->assertEquals( $expected['type'], $results['type'] );
 	}
 
 	/**
@@ -532,13 +532,9 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 		$menus = new WP_Customize_Nav_Menus( $this->wp_customize );
 
 		$menus->customize_preview_init();
-		$this->assertEquals( 10, has_action( 'template_redirect', array( $menus, 'render_menu' ) ) );
 		$this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $menus, 'customize_preview_enqueue_deps' ) ) );
-
-		if ( ! isset( $_REQUEST[ WP_Customize_Nav_Menus::RENDER_QUERY_VAR ] ) ) {
-			$this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) );
-			$this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) );
-		}
+		$this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) );
+		$this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) );
 	}
 
 	/**
@@ -548,37 +544,25 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 	 */
 	function test_filter_wp_nav_menu_args() {
 		do_action( 'customize_register', $this->wp_customize );
-		$menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+		$menus = $this->wp_customize->nav_menus;
 
 		$results = $menus->filter_wp_nav_menu_args( array(
 			'echo'            => true,
 			'fallback_cb'     => 'wp_page_menu',
 			'walker'          => '',
 			'menu'            => wp_create_nav_menu( 'Foo' ),
+			'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
 		) );
-		$this->assertEquals( 1, $results['can_partial_refresh'] );
+		$this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
 
-		$expected = array(
-			'echo',
-			'can_partial_refresh',
-			'fallback_cb',
-			'instance_number',
-			'walker',
-		);
 		$results = $menus->filter_wp_nav_menu_args( array(
 			'echo'            => false,
 			'fallback_cb'     => 'wp_page_menu',
 			'walker'          => new Walker_Nav_Menu(),
+			'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
 		) );
-		$this->assertEqualSets( $expected, array_keys( $results ) );
+		$this->assertArrayNotHasKey( 'customize_preview_nav_menus_args', $results );
 		$this->assertEquals( 'wp_page_menu', $results['fallback_cb'] );
-		$this->assertEquals( 0, $results['can_partial_refresh'] );
-
-		$this->assertNotEmpty( $menus->preview_nav_menu_instance_args[ $results['instance_number'] ] );
-		$preview_nav_menu_instance_args = $menus->preview_nav_menu_instance_args[ $results['instance_number'] ];
-		$this->assertEquals( '', $preview_nav_menu_instance_args['fallback_cb'] );
-		$this->assertEquals( '', $preview_nav_menu_instance_args['walker'] );
-		$this->assertNotEmpty( $preview_nav_menu_instance_args['args_hash'] );
 	}
 
 	/**
@@ -595,19 +579,18 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 			'menu'        => wp_create_nav_menu( 'Foo' ),
 			'fallback_cb' => 'wp_page_menu',
 			'walker'      => '',
+			'items_wrap'  => '<ul id="%1$s" class="%2$s">%3$s</ul>',
 		) );
 
 		ob_start();
 		wp_nav_menu( $args );
 		$nav_menu_content = ob_get_clean();
 
-		$object_args = json_decode( json_encode( $args ), false );
-		$result = $menus->filter_wp_nav_menu( $nav_menu_content, $object_args );
-		$expected = sprintf(
-			'<div class="partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d menu">',
-			$args['instance_number']
-		);
-		$this->assertStringStartsWith( $expected, $result );
+		$result = $menus->filter_wp_nav_menu( $nav_menu_content, (object) $args );
+
+		$this->assertContains( sprintf( ' data-customize-partial-id="nav_menu_instance[%s]"', $args['customize_preview_nav_menus_args']['args_hmac'] ), $result );
+		$this->assertContains( ' data-customize-partial-type="nav_menu_instance"', $result );
+		$this->assertContains( ' data-customize-partial-placement-context="', $result );
 	}
 
 	/**
@@ -622,31 +605,5 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 		$menus->customize_preview_enqueue_deps();
 
 		$this->assertTrue( wp_script_is( 'customize-preview-nav-menus' ) );
-		$this->assertEquals( 10, has_action( 'wp_print_footer_scripts', array( $menus, 'export_preview_data' ) ) );
-	}
-
-	/**
-	 * Test the export_preview_data method.
-	 *
-	 * @see WP_Customize_Nav_Menus::export_preview_data()
-	 */
-	function test_export_preview_data() {
-		do_action( 'customize_register', $this->wp_customize );
-		$menus = new WP_Customize_Nav_Menus( $this->wp_customize );
-
-		$request_uri = $_SERVER['REQUEST_URI'];
-
-		ob_start();
-		$_SERVER['REQUEST_URI'] = '/wp-admin';
-		$menus->export_preview_data();
-		$data = ob_get_clean();
-
-		$_SERVER['REQUEST_URI'] = $request_uri;
-
-		$this->assertContains( '_wpCustomizePreviewNavMenusExports', $data );
-		$this->assertContains( 'renderQueryVar', $data );
-		$this->assertContains( 'renderNonceValue', $data );
-		$this->assertContains( 'renderNoncePostKey', $data );
-		$this->assertContains( 'navMenuInstanceArgs', $data );
 	}
 }
diff --git tests/phpunit/tests/customize/widgets.php tests/phpunit/tests/customize/widgets.php
index cfe57c6..ff7cc4d 100644
--- tests/phpunit/tests/customize/widgets.php
+++ tests/phpunit/tests/customize/widgets.php
@@ -121,7 +121,7 @@ class Tests_WP_Customize_Widgets extends WP_UnitTestCase {
 		$default_args = array(
 			'type' => 'option',
 			'capability' => 'edit_theme_options',
-			'transport' => 'refresh',
+			'transport' => 'postMessage',
 			'default' => array(),
 			'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ),
 			'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ),
@@ -150,7 +150,7 @@ class Tests_WP_Customize_Widgets extends WP_UnitTestCase {
 		$default_args = array(
 			'type' => 'option',
 			'capability' => 'edit_theme_options',
-			'transport' => 'refresh',
+			'transport' => 'postMessage',
 			'default' => array(),
 			'sanitize_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets' ),
 			'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets_js_instance' ),
diff --git tests/qunit/fixtures/customize-menus.js tests/qunit/fixtures/customize-menus.js
index 8478510..0c4c53e 100755
--- tests/qunit/fixtures/customize-menus.js
+++ tests/qunit/fixtures/customize-menus.js
@@ -2,7 +2,7 @@
 window._wpCustomizeNavMenusSettings = {
 	'nonce': 'yo',
 	'phpIntMax': '2147483647',
-	'menuItemTransport': 'postMessage',
+	'settingTransport': 'postMessage',
 	'allMenus': [{
 		'term_id': '2',
 		'name': 'Social Menu',
