diff --git src/bp-core/bp-core-update.php src/bp-core/bp-core-update.php
index a85c41e..daa09e5 100644
--- src/bp-core/bp-core-update.php
+++ src/bp-core/bp-core-update.php
@@ -260,6 +260,10 @@ function bp_version_updater() {
 		if ( $raw_db_version < 9615 ) {
 			bp_update_to_2_3();
 		}
+
+		if ( $raw_db_version < 10457 ) {
+			bp_update_to_2_5();
+		}
 	}
 
 	/** All done! *************************************************************/
@@ -484,6 +488,18 @@ function bp_update_to_2_3() {
 }
 
 /**
+ * 2.5.0 update routine.
+ *
+ * - Kick off cron job to migrate XProfile visibility data.
+ *
+ * @since 2.5.0
+ */
+function bp_update_to_2_5() {
+	bp_update_option( 'bp_xprofile_migrated_field_visibility', 0 );
+	wp_schedule_single_event( time() + ( 1 * MINUTE_IN_SECONDS ), 'bp_xprofile_field_visibility_migrate_hook' );
+}
+
+/**
  * Updates the component field for new_members type.
  *
  * @since 2.2.0
diff --git src/bp-loader.php src/bp-loader.php
index 77d5408..f01f5c6 100644
--- src/bp-loader.php
+++ src/bp-loader.php
@@ -328,7 +328,7 @@ class BuddyPress {
 		/** Versions **********************************************************/
 
 		$this->version    = '2.5.0-alpha';
-		$this->db_version = 10071;
+		$this->db_version = 10457;
 
 		/** Loading ***********************************************************/
 
diff --git src/bp-xprofile/bp-xprofile-functions.php src/bp-xprofile/bp-xprofile-functions.php
index 0996257..285a69a 100644
--- src/bp-xprofile/bp-xprofile-functions.php
+++ src/bp-xprofile/bp-xprofile-functions.php
@@ -470,16 +470,12 @@ function xprofile_set_field_visibility_level( $field_id = 0, $user_id = 0, $visi
 		return false;
 	}
 
-	// Stored in an array in usermeta.
-	$current_visibility_levels = bp_get_user_meta( $user_id, 'bp_xprofile_visibility_levels', true );
-
-	if ( !$current_visibility_levels ) {
-		$current_visibility_levels = array();
+	$data_object = new BP_XProfile_ProfileData( $field_id, $user_id );
+	if ( ! $data_object->exists() ) {
+		return false;
 	}
 
-	$current_visibility_levels[$field_id] = $visibility_level;
-
-	return bp_update_user_meta( $user_id, 'bp_xprofile_visibility_levels', $current_visibility_levels );
+	return (bool) $data_object->set_visibility_level( $visibility_level );
 }
 
 /**
@@ -498,8 +494,8 @@ function xprofile_get_field_visibility_level( $field_id = 0, $user_id = 0 ) {
 		return $current_level;
 	}
 
-	$current_levels = bp_get_user_meta( $user_id, 'bp_xprofile_visibility_levels', true );
-	$current_level  = isset( $current_levels[ $field_id ] ) ? $current_levels[ $field_id ] : '';
+	$data_object   = new BP_XProfile_ProfileData( $field_id, $user_id );
+	$current_level = $data_object->get_visibility_level();
 
 	// Use the user's stored level, unless custom visibility is disabled.
 	$field = xprofile_get_field( $field_id );
@@ -517,6 +513,74 @@ function xprofile_get_field_visibility_level( $field_id = 0, $user_id = 0 ) {
 }
 
 /**
+ * Migrate field visibility from usermeta to xprofilemeta.
+ *
+ * Runs on a cron hook, 'bp_xprofile_field_visibility_migrate_hook'. See bp_update_to_2_5().
+ *
+ * @since BuddyPress 2.5.0
+ */
+function bp_xprofile_field_visibility_migrate() {
+	global $wpdb;
+
+	if ( bp_get_option( 'bp_xprofile_migrated_field_visibility' ) ) {
+		return;
+	}
+
+	bp_update_option( 'bp_xprofile_field_visibility_migration_in_progress', 1 );
+
+	$bp = buddypress();
+
+	$migrate_user_ids = get_users( array(
+		'fields' => 'ids',
+		'number' => 25,
+		'meta_query' => array(
+			array(
+				'key'     => 'bp_xprofile_visibility_levels',
+				'compare' => 'EXISTS',
+			),
+		),
+	) );
+
+	if ( empty( $migrate_user_ids ) ) {
+		// Nothing more to do here.
+		bp_update_option( 'bp_xprofile_migrated_field_visibility', 1 );
+	} else {
+		foreach ( $migrate_user_ids as $u ) {
+			$visibility_levels = bp_get_user_meta( $u, 'bp_xprofile_visibility_levels', true );
+			$failed_levels = array();
+			foreach ( $visibility_levels as $field_id => $level ) {
+				$data = new BP_XProfile_ProfileData( $field_id, $u );
+				if ( ! $data->exists() ) {
+					// If the data doesn't exist, there's nothing to migrate.
+					continue;
+				}
+
+				// If the field already has the visibility level, there's nothing to migrate.
+				if ( $level === $data->get_visibility_level() ) {
+					continue;
+				}
+
+				if ( ! $data->set_visibility_level( $level ) ) {
+					$failed_levels[ $field_id ] = $level;
+				}
+			}
+
+			if ( empty( $failed_levels ) ) {
+				bp_delete_user_meta( $u, 'bp_xprofile_visibility_levels' );
+			} else {
+				bp_update_user_meta( $u, 'bp_xprofile_visibility_levels', $visibility_levels );
+			}
+		}
+
+		// Run again in a minute.
+		wp_schedule_single_event( time() + ( 1 * MINUTE_IN_SECONDS ), 'bp_xprofile_field_visibility_migrate_hook' );
+	}
+
+	bp_delete_option( 'bp_xprofile_field_visibility_migration_in_progress' );
+}
+add_action( 'bp_xprofile_field_visibility_migrate_hook', 'bp_xprofile_field_visibility_migrate' );
+
+/**
  * Delete XProfile field data.
  *
  * @param string $field   Field to delete.
diff --git src/bp-xprofile/classes/class-bp-xprofile-profiledata.php src/bp-xprofile/classes/class-bp-xprofile-profiledata.php
index 7f46cd4..f98f12c 100644
--- src/bp-xprofile/classes/class-bp-xprofile-profiledata.php
+++ src/bp-xprofile/classes/class-bp-xprofile-profiledata.php
@@ -53,6 +53,15 @@ class BP_XProfile_ProfileData {
 	public $last_updated;
 
 	/**
+	 * Data visibility level.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @var string
+	 */
+	protected $visibility_level;
+
+	/**
 	 * BP_XProfile_ProfileData constructor.
 	 *
 	 * @param null $field_id Field ID to instantiate.
@@ -252,6 +261,40 @@ class BP_XProfile_ProfileData {
 		return true;
 	}
 
+	/**
+	 * Get the visibility level for this data.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @return string
+	 */
+	public function get_visibility_level() {
+		if ( null === $this->visibility_level ) {
+			$this->visibility_level = bp_xprofile_get_meta( $this->id, 'data', 'visibility_level', true );
+		}
+
+		return $this->visibility_level;
+	}
+
+	/**
+	 * Set the visibility level for this data.
+	 *
+	 * Does not validate level against whitelist.
+	 *
+	 * @since 2.5.0
+	 *
+	 * @param string $level New visibility level.
+	 * @return bool True on success, false on failure.
+	 */
+	public function set_visibility_level( $level ) {
+		if ( ! bp_xprofile_update_meta( $this->id, 'data', 'visibility_level', $level ) ) {
+			return false;
+		}
+
+		$this->visibility_level = null;
+		return true;
+	}
+
 	/** Static Methods ********************************************************/
 
 	/**
diff --git tests/phpunit/testcases/xprofile/functions.php tests/phpunit/testcases/xprofile/functions.php
index f5f56e4..a0a14dd 100644
--- tests/phpunit/testcases/xprofile/functions.php
+++ tests/phpunit/testcases/xprofile/functions.php
@@ -102,6 +102,7 @@ Bar!';
 		$f = $this->factory->xprofile_field->create( array(
 			'field_group_id' => $g,
 		) );
+		xprofile_set_field_data( $f, $u, 'foo' );
 
 		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
 		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'allowed' );
@@ -120,6 +121,7 @@ Bar!';
 		$f = $this->factory->xprofile_field->create( array(
 			'field_group_id' => $g,
 		) );
+		xprofile_set_field_data( $f, $u, 'foo' );
 
 		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
 		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'allowed' );
@@ -137,6 +139,7 @@ Bar!';
 		$f = $this->factory->xprofile_field->create( array(
 			'field_group_id' => $g,
 		) );
+		xprofile_set_field_data( $f, $u, 'foo' );
 
 		bp_xprofile_update_meta( $f, 'field', 'default_visibility', 'adminsonly' );
 		bp_xprofile_update_meta( $f, 'field', 'allow_custom_visibility', 'disabled' );
@@ -147,6 +150,48 @@ Bar!';
 	}
 
 	/**
+	 * @group bp_xprofile_field_visibility_migrate
+	 */
+	public function test_bp_xprofile_field_visibility_migrate_should_move_all_data() {
+		$users = $this->factory->user->create_many( 2 );
+		$g = $this->factory->xprofile_group->create();
+		$f1 = $this->factory->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+		$f2 = $this->factory->xprofile_field->create( array(
+			'field_group_id' => $g,
+		) );
+
+		// Set up and verify old-style data.
+		foreach ( $users as $user ) {
+			xprofile_set_field_data( $f1, $user, 'foo' );
+			xprofile_set_field_data( $f2, $user, 'foo' );
+
+			bp_update_user_meta( $user, 'bp_xprofile_visibility_levels', array(
+				$f1 => 'loggedin',
+				$f2 => 'adminsonly',
+			) );
+
+			$data1 = new BP_XProfile_ProfileData( $f1, $user );
+			$data2 = new BP_XProfile_ProfileData( $f2, $user );
+
+			$this->assertSame( '', $data1->get_visibility_level() );
+			$this->assertSame( '', $data2->get_visibility_level() );
+		}
+
+		bp_xprofile_field_visibility_migrate();
+
+		foreach ( $users as $user ) {
+			$data1 = new BP_XProfile_ProfileData( $f1, $user );
+			$data2 = new BP_XProfile_ProfileData( $f2, $user );
+
+			$this->assertSame( '', bp_get_user_meta( $user, 'bp_xprofile_visibility_levels', true ) );
+			$this->assertSame( 'loggedin', $data1->get_visibility_level() );
+			$this->assertSame( 'adminsonly', $data2->get_visibility_level() );
+		}
+	}
+
+	/**
 	 * @group xprofilemeta
 	 * @group bp_xprofile_delete_meta
 	 */
