diff --git src/wp-includes/class-wp-customize-setting.php src/wp-includes/class-wp-customize-setting.php
index 98f37f9..637940b 100644
--- src/wp-includes/class-wp-customize-setting.php
+++ src/wp-includes/class-wp-customize-setting.php
@@ -153,19 +153,40 @@ class WP_Customize_Setting {
 	protected $_original_value;
 
 	/**
-	 * Set up filters for the setting so that the preview request
-	 * will render the drafted changes.
+	 * Add filters to supply the setting's value when accessed.
+	 *
+	 * If the setting already has a pre-existing value and there is no incoming
+	 * post value for the setting, then this method will short-circuit since
+	 * there is no change to preview.
 	 *
 	 * @since 3.4.0
+	 * @since 4.4.0 Added boolean return value.
+	 * @access public
+	 *
+	 * @return bool False when preview short-circuits due no change needing to be previewed.
 	 */
 	public function preview() {
-		if ( ! isset( $this->_original_value ) ) {
-			$this->_original_value = $this->value();
-		}
 		if ( ! isset( $this->_previewed_blog_id ) ) {
 			$this->_previewed_blog_id = get_current_blog_id();
 		}
 
+		/*
+		 * Check if the setting has a pre-existing value (an isset check),
+		 * and if doesn't have any incoming post value. If both checks are true,
+		 * then the preview short-circuits because there is nothing that needs
+		 * to be previewed.
+		 */
+		$undefined = new stdClass();
+		$default = $this->default;
+		$this->default = $undefined; // Temporarily set default to undefined so we can detect if existing value is set.
+		$value = $this->value();
+		$this->default = $default;
+		$isset = ( $undefined !== $value );
+		if ( $isset && $undefined === $this->post_value( $undefined ) ) {
+			return false;
+		}
+
+		$this->_original_value = ( $isset ? $value : $default );
 		switch( $this->type ) {
 			case 'theme_mod' :
 				add_filter( 'theme_mod_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
@@ -204,6 +225,7 @@ class WP_Customize_Setting {
 				 */
 				do_action( "customize_preview_{$this->type}", $this );
 		}
+		return true;
 	}
 
 	/**
@@ -224,15 +246,13 @@ class WP_Customize_Setting {
 			return $original;
 		}
 
-		$undefined = new stdClass(); // symbol hack
-		$post_value = $this->post_value( $undefined );
-		if ( $undefined === $post_value ) {
-			$value = $this->_original_value;
+		$value = $this->post_value( $this->default );
+
+		if ( empty( $this->id_data['keys'] ) ) {
+			return $value;
 		} else {
-			$value = $post_value;
+			return $this->multidimensional_replace( $original, $this->id_data['keys'], $value );
 		}
-
-		return $this->multidimensional_replace( $original, $this->id_data['keys'], $value );
 	}
 
 	/**
@@ -984,13 +1004,23 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
 	 * Handle previewing the setting.
 	 *
 	 * @since 4.3.0
+	 * @since 4.4.0 Added boolean return value.
 	 * @access public
 	 *
 	 * @see WP_Customize_Manager::post_value()
+	 *
+	 * @return bool False if method short-circuited due to no-op.
 	 */
 	public function preview() {
 		if ( $this->is_previewed ) {
-			return;
+			return false;
+		}
+
+		$undefined = new stdClass();
+		$is_placeholder = ( $this->post_id < 0 );
+		$is_dirty = ( $undefined !== $this->post_value( $undefined ) );
+		if ( ! $is_placeholder && ! $is_dirty ) {
+			return false;
 		}
 
 		$this->is_previewed              = true;
@@ -1006,6 +1036,8 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
 		}
 
 		// @todo Add get_post_metadata filters for plugins to add their data.
+
+		return true;
 	}
 
 	/**
@@ -1618,13 +1650,23 @@ class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
 	 * Handle previewing the setting.
 	 *
 	 * @since 4.3.0
+	 * @since 4.4.0 Added boolean return value
 	 * @access public
 	 *
 	 * @see WP_Customize_Manager::post_value()
+	 *
+	 * @return bool False if method short-circuited due to no-op.
 	 */
 	public function preview() {
 		if ( $this->is_previewed ) {
-			return;
+			return false;
+		}
+
+		$undefined = new stdClass();
+		$is_placeholder = ( $this->term_id < 0 );
+		$is_dirty = ( $undefined !== $this->post_value( $undefined ) );
+		if ( ! $is_placeholder && ! $is_dirty ) {
+			return false;
 		}
 
 		$this->is_previewed       = true;
@@ -1635,6 +1677,8 @@ class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
 		add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
 		add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
 		add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
+
+		return true;
 	}
 
 	/**
diff --git tests/phpunit/tests/customize/setting.php tests/phpunit/tests/customize/setting.php
index 08bbc65..f3accbb 100644
--- tests/phpunit/tests/customize/setting.php
+++ tests/phpunit/tests/customize/setting.php
@@ -102,7 +102,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 			$setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
 			$this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
 			$this->assertEquals( $default, $setting->value() );
-			$setting->preview();
+			$this->assertTrue( $setting->preview(), 'Preview should not no-op since setting has no existing value.' );
 			$this->assertEquals( $default, call_user_func( $type_options['getter'], $name, $this->undefined ), sprintf( 'Expected %s(%s) to return setting default: %s.', $type_options['getter'], $name, $default ) );
 			$this->assertEquals( $default, $setting->value() );
 
@@ -114,18 +114,18 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 			$setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
 			$this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
 			$this->assertEquals( $initial_value, $setting->value() );
-			$setting->preview();
+			$this->assertFalse( $setting->preview(), 'Preview should no-op since setting value was extant and no post value was present.' );
 			$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
 			$this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
 			$this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
 			$this->assertEquals( $initial_value, $setting->value() );
 
-			// @todo What if we call the setter after preview() is called? If no post_value, should the new set value be stored? If that happens, then the following 3 assertions should be inverted
 			$overridden_value = "overridden_value_$name";
 			call_user_func( $type_options['setter'], $name, $overridden_value );
-			$this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
-			$this->assertEquals( $initial_value, $setting->value() );
-			$this->assertNotEquals( $overridden_value, $setting->value() );
+			$message = 'Initial value should be overridden because initial preview() was no-op due to setting having existing value and/or post value was absent.';
+			$this->assertEquals( $overridden_value, call_user_func( $type_options['getter'], $name ), $message );
+			$this->assertEquals( $overridden_value, $setting->value(), $message );
+			$this->assertNotEquals( $initial_value, $setting->value(), $message );
 
 			// Non-multidimensional: Test unset setting being overridden by a post value
 			$name = "unset_{$type}_overridden";
@@ -133,7 +133,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 			$setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
 			$this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
 			$this->assertEquals( $default, $setting->value() );
-			$setting->preview(); // activate post_data
+			$this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data
 			$this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
 			$this->assertEquals( $this->post_data_overrides[ $name ], $setting->value() );
 
@@ -145,7 +145,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 			$setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
 			$this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name, $this->undefined ) );
 			$this->assertEquals( $initial_value, $setting->value() );
-			$setting->preview(); // activate post_data
+			$this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data
 			$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
 			$this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
 			$this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
@@ -167,7 +167,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 			$setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
 			$this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
 			$this->assertEquals( $default, $setting->value() );
-			$setting->preview();
+			$this->assertTrue( $setting->preview() );
 			$base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
 			$this->assertArrayHasKey( 'foo', $base_value );
 			$this->assertEquals( $default, $base_value['foo'] );
@@ -311,8 +311,8 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 		$this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
 		$this->assertEquals( $initial_value, $setting->value() );
 		$setting->preview();
-		$this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
-		$this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) );
+		$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ), 'Zero preview actions because initial value is set with no incoming post value, so there is no preview to apply.' );
+		$this->assertEquals( 1, did_action( "customize_preview_{$setting->type}" ) );
 		$this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // should be same as above
 		$this->assertEquals( $initial_value, $setting->value() ); // should be same as above
 
@@ -325,8 +325,8 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 		$this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
 		$this->assertEquals( $default, $setting->value() );
 		$setting->preview();
-		$this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
-		$this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) );
+		$this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ), 'One preview action now because initial value was not set and/or there is no incoming post value, so there is is a preview to apply.' );
+		$this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) );
 		$this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
 		$this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
 
@@ -342,7 +342,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 		$this->assertEquals( $initial_value, $setting->value() );
 		$setting->preview();
 		$this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
-		$this->assertEquals( 4, did_action( "customize_preview_{$setting->type}" ) );
+		$this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) );
 		$this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
 		$this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
 
