[dart2js] Unmodifiable Array and typed data using flags

- Add SSA HArrayFlags{Check,Get,Set} instructions to check for
  fixed-length and unmodifiable JSArray and JavaScript typed data
  instances.

- Add optimizations to remove redundant checks.

- Added HOutputConstrained interface for instructions that must have
  the input and output in the same JavaScript variable. This
  generalizes some code generation logic already applied to HCheck.

Bug: #53785
Change-Id: I61750dd03aa3a964eed3bc76e1656c5f60f77109
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/372002
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Mayank Patke <fishythefish@google.com>
diff --git a/pkg/compiler/lib/src/common/elements.dart b/pkg/compiler/lib/src/common/elements.dart
index 04e82de..3976400 100644
--- a/pkg/compiler/lib/src/common/elements.dart
+++ b/pkg/compiler/lib/src/common/elements.dart
@@ -820,6 +820,9 @@
   FunctionEntity get cyclicThrowHelper =>
       _findHelperFunction("throwCyclicInit");
 
+  FunctionEntity get throwUnsupportedOperation =>
+      _findHelperFunction('throwUnsupportedOperation');
+
   FunctionEntity get defineProperty => _findHelperFunction('defineProperty');
 
   FunctionEntity get throwLateFieldNI =>
diff --git a/pkg/compiler/lib/src/js_backend/namer.dart b/pkg/compiler/lib/src/js_backend/namer.dart
index f80e8d6..a4da9b5 100644
--- a/pkg/compiler/lib/src/js_backend/namer.dart
+++ b/pkg/compiler/lib/src/js_backend/namer.dart
@@ -2192,6 +2192,8 @@
 
   String get recordShapeRecipe => r'$recipe';
   String get recordShapeTag => r'$shape';
+
+  String get arrayFlagsPropertyName => r'$flags';
 }
 
 /// Minified version of the fixed names usage by the namer.
diff --git a/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart b/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart
index 5b9d608..5c6ab76 100644
--- a/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart
+++ b/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart
@@ -4,6 +4,7 @@
 
 library dart2js.js_emitter.interceptor_stub_generator;
 
+import 'package:js_runtime/synced/array_flags.dart';
 import 'package:js_runtime/synced/embedded_names.dart' as embeddedNames;
 
 import '../common/elements.dart';
@@ -409,10 +410,14 @@
 
         return js.statement(r'''
           if (typeof a0 === "number")
-            if (# && !receiver.immutable$list &&
+            if (# && !(receiver.# & #) &&
                 (a0 >>> 0) === a0 && a0 < receiver.length)
               return receiver[a0] = a1;
-          ''', typeCheck);
+          ''', [
+          typeCheck,
+          _namer.fixedNames.arrayFlagsPropertyName,
+          js.number(ArrayFlags.unmodifiableCheck)
+        ]);
       }
     } else if (selector.isCall) {
       if (selector.name == 'abs' && selector.argumentCount == 0) {
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
index 9749e04..1f5ed26 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
@@ -171,11 +171,7 @@
 //
 // The runtime ensures that const-lists cannot be modified.
 function makeConstList(list) {
-  // By assigning a function to the properties they become part of the
-  // hidden class. The actual values of the fields don't matter, since we
-  // only check if they exist.
-  list.immutable\$list = Array;
-  list.fixed\$length = Array;
+  list.#arrayFlagsProperty = ${ArrayFlags.constant};
   return list;
 }
 
@@ -675,6 +671,7 @@
       'directAccessTestExpression': js.js(_directAccessTestExpression),
       'throwLateFieldADI': _emitter
           .staticFunctionAccess(_closedWorld.commonElements.throwLateFieldADI),
+      'arrayFlagsProperty': js.string(_namer.fixedNames.arrayFlagsPropertyName),
       'operatorIsPrefix': js.string(_namer.fixedNames.operatorIsPrefix),
       'tearOffCode': js.Block(
           buildTearOffCode(_options, _emitter, _closedWorld.commonElements)),
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart
index 7870c0d..74ac4ee 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart
@@ -6,6 +6,8 @@
 
 import 'dart:convert' show JsonEncoder;
 
+import 'package:js_runtime/synced/array_flags.dart' show ArrayFlags;
+
 import 'package:js_runtime/synced/embedded_names.dart'
     show
         DEFERRED_INITIALIZED,
@@ -35,6 +37,7 @@
         RTI_UNIVERSE,
         RtiUniverseFieldNames,
         TYPES;
+
 import 'package:js_shared/variance.dart';
 
 import 'package:js_ast/src/precedence.dart' as js_precedence;
diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart
index dea373b..444b9b8 100644
--- a/pkg/compiler/lib/src/kernel/dart2js_target.dart
+++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart
@@ -255,6 +255,7 @@
 // compile-platform should just specify which libraries to compile instead.
 const requiredLibraries = <String, List<String>>{
   'dart2js': [
+    'dart:_array_flags',
     'dart:_async_status_codes',
     'dart:_dart2js_only',
     'dart:_dart2js_runtime_metrics',
@@ -297,6 +298,7 @@
     'dart:web_gl',
   ],
   'dart2js_server': [
+    'dart:_array_flags',
     'dart:_async_status_codes',
     'dart:_dart2js_only',
     'dart:_dart2js_runtime_metrics',
diff --git a/pkg/compiler/lib/src/ssa/builder.dart b/pkg/compiler/lib/src/ssa/builder.dart
index 5d10b1e..63e4850 100644
--- a/pkg/compiler/lib/src/ssa/builder.dart
+++ b/pkg/compiler/lib/src/ssa/builder.dart
@@ -4631,6 +4631,12 @@
       _handleLateInitializeOnceCheck(invocation, sourceInformation);
     } else if (name == 'HCharCodeAt') {
       _handleCharCodeAt(invocation, sourceInformation);
+    } else if (name == 'HArrayFlagsGet') {
+      _handleArrayFlagsGet(invocation, sourceInformation);
+    } else if (name == 'HArrayFlagsSet') {
+      _handleArrayFlagsSet(invocation, sourceInformation);
+    } else if (name == 'HArrayFlagsCheck') {
+      _handleArrayFlagsCheck(invocation, sourceInformation);
     } else {
       reporter.internalError(
           _elementMap.getSpannable(targetElement, invocation),
@@ -5372,6 +5378,73 @@
       ..sourceInformation = sourceInformation);
   }
 
+  void _handleArrayFlagsGet(
+      ir.StaticInvocation invocation, SourceInformation? sourceInformation) {
+    if (_unexpectedForeignArguments(invocation,
+        minPositional: 1, maxPositional: 1)) {
+      // Result expected on stack.
+      stack.add(graph.addConstantNull(closedWorld));
+      return;
+    }
+    List<HInstruction> inputs = _visitPositionalArguments(invocation.arguments);
+    final array = inputs.single;
+
+    push(HArrayFlagsGet(array, _abstractValueDomain.uint31Type)
+      ..sourceInformation = sourceInformation);
+  }
+
+  void _handleArrayFlagsSet(
+      ir.StaticInvocation invocation, SourceInformation? sourceInformation) {
+    if (_unexpectedForeignArguments(invocation,
+        minPositional: 2, maxPositional: 2, typeArgumentCount: 1)) {
+      // Result expected on stack.
+      stack.add(graph.addConstantNull(closedWorld));
+      return;
+    }
+
+    List<HInstruction> inputs = _visitPositionalArguments(invocation.arguments);
+    final array = inputs[0];
+    final flags = inputs[1];
+
+    // TODO(sra): Use the flags to improve in the AbstractValue, which may
+    // contain powerset domain bits outside of the conventional type
+    // system. Perhaps do this in types_propagation.
+    DartType type = _getDartTypeIfValid(invocation.arguments.types.single);
+    AbstractValue? instructionType = _typeBuilder.trustTypeMask(type);
+    // TODO(sra): Better type
+    instructionType ??= _abstractValueDomain.dynamicType;
+
+    push(HArrayFlagsSet(array, flags, instructionType)
+      ..sourceInformation = sourceInformation);
+  }
+
+  void _handleArrayFlagsCheck(
+      ir.StaticInvocation invocation, SourceInformation? sourceInformation) {
+    if (_unexpectedForeignArguments(invocation,
+        minPositional: 4, maxPositional: 4, typeArgumentCount: 1)) {
+      // Result expected on stack.
+      stack.add(graph.addConstantNull(closedWorld));
+      return;
+    }
+    List<HInstruction> inputs = _visitPositionalArguments(invocation.arguments);
+    final array = inputs[0];
+    final arrayFlags = inputs[1];
+    final checkFlags = inputs[2];
+    final operation = inputs[3];
+
+    // TODO(sra): Use the flags to improve in the AbstractValue, which may
+    // contain powerset domain bits outside of the conventional type
+    // system. Perhaps do this in types_propagation.
+    DartType type = _getDartTypeIfValid(invocation.arguments.types.single);
+    AbstractValue? instructionType = _typeBuilder.trustTypeMask(type);
+    // TODO(sra): Better type
+    instructionType ??= _abstractValueDomain.dynamicType;
+
+    push(HArrayFlagsCheck(
+        array, arrayFlags, checkFlags, operation, instructionType)
+      ..sourceInformation = sourceInformation);
+  }
+
   void _handleForeignTypeRef(
       ir.StaticInvocation invocation, SourceInformation? sourceInformation) {
     if (_unexpectedForeignArguments(invocation,
diff --git a/pkg/compiler/lib/src/ssa/codegen.dart b/pkg/compiler/lib/src/ssa/codegen.dart
index 5785b8d..4fa574d 100644
--- a/pkg/compiler/lib/src/ssa/codegen.dart
+++ b/pkg/compiler/lib/src/ssa/codegen.dart
@@ -682,14 +682,15 @@
     // emit an assignment, because the intTypeCheck just returns its
     // argument.
     bool needsAssignment = true;
-    if (instruction is HCheck) {
+    if (instruction is HOutputConstrainedToAnInput) {
       if (instruction is HPrimitiveCheck ||
           instruction is HAsCheck ||
           instruction is HAsCheckSimple ||
           instruction is HBoolConversion ||
           instruction is HNullCheck ||
-          instruction is HLateReadCheck) {
-        String? inputName = variableNames.getName(instruction.checkedInput);
+          instruction is HLateReadCheck ||
+          instruction is HArrayFlagsSet) {
+        String? inputName = variableNames.getName(instruction.constrainedInput);
         if (variableNames.getName(instruction) == inputName) {
           needsAssignment = false;
         }
@@ -715,9 +716,9 @@
     }
   }
 
-  HInstruction skipGenerateAtUseCheckInputs(HCheck check) {
-    HInstruction input = check.checkedInput;
-    if (input is HCheck && isGenerateAtUseSite(input)) {
+  HInstruction skipGenerateAtUseCheckInputs(HOutputConstrainedToAnInput check) {
+    HInstruction input = check.constrainedInput;
+    if (input is HOutputConstrainedToAnInput && isGenerateAtUseSite(input)) {
       return skipGenerateAtUseCheckInputs(input);
     }
     return input;
@@ -726,7 +727,8 @@
   void use(HInstruction argument) {
     if (isGenerateAtUseSite(argument)) {
       visitExpression(argument);
-    } else if (argument is HCheck && !variableNames.hasName(argument)) {
+    } else if (argument is HOutputConstrainedToAnInput &&
+        !variableNames.hasName(argument)) {
       // We have a check that is not generate-at-use and has no name, yet is a
       // subexpression (we are in 'use'). This happens when we have a chain of
       // checks on an available unnamed value (e.g. a constant). The checks are
@@ -737,11 +739,10 @@
       // instruction has a name or is generate-at-use". This would require
       // naming the input or output of the chain-of-checks.
 
-      HCheck check = argument;
       // This can only happen if the checked node also does not have a name.
-      assert(!variableNames.hasName(check.checkedInput));
+      assert(!variableNames.hasName(argument.constrainedInput));
 
-      use(skipGenerateAtUseCheckInputs(check));
+      use(skipGenerateAtUseCheckInputs(argument));
     } else {
       assert(variableNames.hasName(argument));
       push(js.VariableUse(variableNames.getName(argument)!));
@@ -3441,4 +3442,69 @@
     _metrics.countHIsLateSentinel++;
     _emitIsLateSentinel(node.inputs.single, node.sourceInformation);
   }
+
+  @override
+  void visitArrayFlagsGet(HArrayFlagsGet node) {
+    use(node.inputs.single);
+    js.Expression array = pop();
+    js.Expression flags =
+        js.js(r'#.#', [array, _namer.fixedNames.arrayFlagsPropertyName]);
+    if (isGenerateAtUseSite(node) && node.usedBy.single is HArrayFlagsCheck) {
+      // The enclosing expression will be an immediate `& mask`.
+      push(flags);
+    } else {
+      // The flags are reused, possibly hoisted, so force an `undefined` to be a
+      // small integer once rather than at each check.
+      push(js.js(r'# | 0', flags));
+    }
+  }
+
+  @override
+  void visitArrayFlagsSet(HArrayFlagsSet node) {
+    use(node.inputs[0]);
+    js.Expression array = pop();
+    use(node.inputs[1]);
+    js.Expression arrayFlags = pop();
+    pushStatement(js.js.statement(r'#.# = #;', [
+      array,
+      _namer.fixedNames.arrayFlagsPropertyName,
+      arrayFlags
+    ]).withSourceInformation(node.sourceInformation));
+  }
+
+  @override
+  void visitArrayFlagsCheck(HArrayFlagsCheck node) {
+    use(node.array);
+    js.Expression array = pop();
+
+    js.Expression? test;
+    if (!node.alwaysThrows()) {
+      use(node.arrayFlags);
+      js.Expression arrayFlags = pop();
+      use(node.checkFlags);
+      js.Expression checkFlags = pop();
+      test = js.js('# & #', [arrayFlags, checkFlags]);
+    }
+
+    final operation = node.operation;
+    // Most common operation is "[]=", so 'pass' that by leaving it out.
+    if (operation
+        case HConstant(constant: StringConstantValue(stringValue: '[]='))) {
+      _pushCallStatic(_commonElements.throwUnsupportedOperation, [array],
+          node.sourceInformation);
+    } else {
+      use(operation);
+      _pushCallStatic(_commonElements.throwUnsupportedOperation, [array, pop()],
+          node.sourceInformation);
+    }
+
+    js.Statement check;
+    if (test == null) {
+      check = js.js.statement('#;', pop());
+    } else {
+      check = js.js.statement('# && #;', [test, pop()]);
+    }
+
+    pushStatement(check.withSourceInformation(node.sourceInformation));
+  }
 }
diff --git a/pkg/compiler/lib/src/ssa/codegen_helpers.dart b/pkg/compiler/lib/src/ssa/codegen_helpers.dart
index dcee553..9f990d2 100644
--- a/pkg/compiler/lib/src/ssa/codegen_helpers.dart
+++ b/pkg/compiler/lib/src/ssa/codegen_helpers.dart
@@ -1041,6 +1041,20 @@
     }
   }
 
+  @override
+  void visitArrayFlagsSet(HArrayFlagsSet instruction) {
+    // Cannot generate-at-use the array input, it is an alias for the value of
+    // this instruction and need to be allocated to a variable.
+    analyzeInputs(instruction, 1);
+  }
+
+  @override
+  void visitArrayFlagsCheck(HArrayFlagsCheck instruction) {
+    // Cannot generate-at-use the array input, it is an alias for the value of
+    // this instruction and need to be allocated to a variable.
+    analyzeInputs(instruction, 1);
+  }
+
   void tryGenerateAtUseSite(HInstruction instruction) {
     if (instruction.isControlFlow()) return;
     markAsGenerateAtUseSite(instruction);
diff --git a/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart b/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart
index 05fb3d4..0e6fc59 100644
--- a/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart
+++ b/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:js_runtime/synced/array_flags.dart' show ArrayFlags;
 import '../common/elements.dart' show JCommonElements;
 import '../constants/constant_system.dart' as constant_system;
 import '../constants/values.dart';
@@ -203,13 +204,23 @@
       OptimizationTestLog? log) {
     HInstruction receiver = instruction.inputs[1];
     HInstruction index = instruction.inputs[2];
-    if (receiver
-        .isMutableIndexable(closedWorld.abstractValueDomain)
-        .isPotentiallyFalse) {
-      return null;
+    final abstractValueDomain = closedWorld.abstractValueDomain;
+
+    bool needsMutableCheck = false;
+    if (abstractValueDomain
+        .isTypedArray(receiver.instructionType)
+        .isDefinitelyTrue) {
+      needsMutableCheck = true;
+    } else if (receiver.isArray(abstractValueDomain).isDefinitelyTrue) {
+      needsMutableCheck =
+          receiver.isMutableArray(abstractValueDomain).isPotentiallyFalse;
+    } else {
+      if (receiver.isMutableIndexable(abstractValueDomain).isPotentiallyFalse) {
+        return null;
+      }
     }
     // TODO(johnniwinther): Merge this and the following if statement.
-    if (index.isInteger(closedWorld.abstractValueDomain).isPotentiallyFalse &&
+    if (index.isInteger(abstractValueDomain).isPotentiallyFalse &&
         // TODO(johnniwinther): Support annotations on the possible targets
         // and used their parameter check policy here.
         closedWorld.annotationsData.getParameterCheckPolicy(null).isEmitted) {
@@ -227,6 +238,24 @@
       }
     }
 
+    if (needsMutableCheck) {
+      HInstruction getFlags =
+          HArrayFlagsGet(receiver, abstractValueDomain.uint31Type)
+            ..sourceInformation = instruction.sourceInformation;
+      instruction.block!.addBefore(instruction, getFlags);
+      HInstruction mask =
+          graph.addConstantInt(ArrayFlags.unmodifiableCheck, closedWorld);
+      HInstruction name = graph.addConstantString('[]=', closedWorld);
+      final instructionType = receiver.instructionType;
+      final checkFlags =
+          HArrayFlagsCheck(receiver, getFlags, mask, name, instructionType)
+            ..sourceInformation = instruction.sourceInformation;
+      instruction.block!.addBefore(instruction, checkFlags);
+      checkFlags.instructionType = checkFlags.computeInstructionType(
+          instructionType, abstractValueDomain);
+      receiver = checkFlags;
+    }
+
     HInstruction checkedIndex = index;
     if (requiresBoundsCheck(instruction, closedWorld)) {
       checkedIndex =
diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart
index d8ab7bb..e55719b 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -130,6 +130,10 @@
   R visitInstanceEnvironment(HInstanceEnvironment node);
   R visitTypeEval(HTypeEval node);
   R visitTypeBind(HTypeBind node);
+
+  R visitArrayFlagsCheck(HArrayFlagsCheck node);
+  R visitArrayFlagsGet(HArrayFlagsGet node);
+  R visitArrayFlagsSet(HArrayFlagsSet node);
 }
 
 abstract class HGraphVisitor {
@@ -644,6 +648,13 @@
   R visitTypeEval(HTypeEval node) => visitInstruction(node);
   @override
   R visitTypeBind(HTypeBind node) => visitInstruction(node);
+
+  @override
+  R visitArrayFlagsCheck(HArrayFlagsCheck node) => visitCheck(node);
+  @override
+  R visitArrayFlagsGet(HArrayFlagsGet node) => visitInstruction(node);
+  @override
+  R visitArrayFlagsSet(HArrayFlagsSet node) => visitInstruction(node);
 }
 
 class SubGraph {
@@ -1108,6 +1119,8 @@
   lateWriteOnceCheck,
   lateInitializeOnceCheck,
   charCodeAt,
+  arrayFlagsGet,
+  arrayFlagsCheck,
 }
 
 abstract class HInstruction implements SpannableWithEntity {
@@ -1607,11 +1620,21 @@
 /// generating JavaScript.
 abstract interface class HLateInstruction {}
 
+/// Interface for instructions where the output is constrained to be one of the
+/// inputs. Used for checks, where the SSA value of the check represents the
+/// same value as the input, but restricted in some way, e.g., being of a
+/// refined type or in a checked range.
+abstract interface class HOutputConstrainedToAnInput implements HInstruction {
+  /// The input which is the 'same' as the output.
+  HInstruction get constrainedInput;
+}
+
 /// A [HCheck] instruction is an instruction that might do a dynamic check at
 /// runtime on an input instruction. To have proper instruction dependencies in
 /// the graph, instructions that depend on the check being done reference the
 /// [HCheck] instruction instead of the input instruction.
-abstract class HCheck extends HInstruction {
+abstract class HCheck extends HInstruction
+    implements HOutputConstrainedToAnInput {
   HCheck(super.inputs, super.type) {
     setUseGvn();
   }
@@ -1625,6 +1648,9 @@
   HInstruction get checkedInput => inputs[0];
 
   @override
+  HInstruction get constrainedInput => checkedInput;
+
+  @override
   bool isJsStatement() => true;
 
   @override
@@ -4674,6 +4700,122 @@
   String toString() => 'HTypeBind()';
 }
 
+/// Check Array or TypedData for permission to modify or grow.
+///
+/// Typical use to check modifiability for `a[i] = 0`. The array flags are
+/// checked to see if there is a bit that prohibits modification.
+///
+///     a = ...
+///     f = HArrayFlagsGet(a);
+///     a2 = HArrayFlagsCheck(a, f, ArrayFlags.unmodifiableCheck, "[]=")
+///     a2[i] = 0
+///
+/// HArrayFlagsGet is a separate instruction so that 'loading' the flags from
+/// the Array can by hoisted.
+class HArrayFlagsCheck extends HCheck {
+  HArrayFlagsCheck(HInstruction array, HInstruction arrayFlags,
+      HInstruction checkFlags, HInstruction operation, AbstractValue type)
+      : super([array, arrayFlags, checkFlags, operation], type);
+
+  HInstruction get array => inputs[0];
+  HInstruction get arrayFlags => inputs[1];
+  HInstruction get checkFlags => inputs[2];
+  HInstruction get operation => inputs[3];
+
+  // The checked type is the input type, refined to match the flags.
+  AbstractValue computeInstructionType(
+      AbstractValue inputType, AbstractValueDomain domain) {
+    // TODO(sra): Depening on the checked flags, the output is fixed-length or
+    // unmodifiable. Refine the type to the degree an AbstractValue can express
+    // that.
+    return inputType;
+  }
+
+  bool alwaysThrows() {
+    if ((arrayFlags, checkFlags)
+        case (
+          HConstant(constant: IntConstantValue(intValue: final arrayBits)),
+          HConstant(constant: IntConstantValue(intValue: final checkBits))
+        ) when arrayBits & checkBits != BigInt.zero) {
+      return true;
+    }
+    return false;
+  }
+
+  @override
+  R accept<R>(HVisitor<R> visitor) => visitor.visitArrayFlagsCheck(this);
+
+  @override
+  bool isControlFlow() => true;
+  @override
+  bool isJsStatement() => true;
+
+  @override
+  _GvnType get _gvnType => _GvnType.arrayFlagsCheck;
+
+  @override
+  bool typeEquals(HInstruction other) => other is HArrayFlagsCheck;
+
+  @override
+  bool dataEquals(HArrayFlagsCheck other) => true;
+}
+
+class HArrayFlagsGet extends HInstruction {
+  HArrayFlagsGet(HInstruction array, AbstractValue type)
+      : super([array], type) {
+    sideEffects.clearAllSideEffects();
+    sideEffects.clearAllDependencies();
+    // Dependency on HArrayFlagsSet.
+    sideEffects.setDependsOnInstancePropertyStore();
+    setUseGvn();
+  }
+
+  @override
+  R accept<R>(HVisitor<R> visitor) => visitor.visitArrayFlagsGet(this);
+
+  @override
+  _GvnType get _gvnType => _GvnType.arrayFlagsGet;
+
+  @override
+  bool typeEquals(HInstruction other) => other is HArrayFlagsGet;
+
+  @override
+  bool dataEquals(HArrayFlagsGet other) => true;
+}
+
+/// Tag an Array or TypedData object to mark it as unmodifiable or fixed-length.
+///
+/// The HArrayFlagsSet instruction represents the tagged Array or TypedData
+/// object. The instruction type can be different to the `array` input.
+/// HArrayFlagsSet is used in a 'linear' style - there are no accesses to the
+/// input after this operation.
+///
+/// To ensure that HArrayFlagsGet (possibly from inlined code) does not float
+/// past HArrayFlagsSet, we use the 'instance property' effect.
+class HArrayFlagsSet extends HInstruction
+    implements HOutputConstrainedToAnInput {
+  HArrayFlagsSet(HInstruction array, HInstruction flags, AbstractValue type)
+      : super([array, flags], type) {
+    // For correct ordering with respect to HArrayFlagsGet:
+    sideEffects.setChangesInstanceProperty();
+    // Be conservative and make HArrayFlagsSet be a memory fence:
+    sideEffects.setAllSideEffects();
+    sideEffects.setDependsOnSomething();
+  }
+
+  HInstruction get array => inputs[0];
+  HInstruction get flags => inputs[1];
+
+  @override
+  HInstruction get constrainedInput => array;
+
+  @override
+  R accept<R>(HVisitor<R> visitor) => visitor.visitArrayFlagsSet(this);
+
+  @override
+  bool isJsStatement() => true;
+}
+
 class HIsLateSentinel extends HInstruction {
   HIsLateSentinel(super.value, super.type) : super._1() {
     setUseGvn();
diff --git a/pkg/compiler/lib/src/ssa/optimize.dart b/pkg/compiler/lib/src/ssa/optimize.dart
index c2132f5..624c66b 100644
--- a/pkg/compiler/lib/src/ssa/optimize.dart
+++ b/pkg/compiler/lib/src/ssa/optimize.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:js_runtime/synced/array_flags.dart' show ArrayFlags;
+
 import '../common.dart';
 import '../common/codegen.dart' show CodegenRegistry;
 import '../common/elements.dart' show JCommonElements;
@@ -255,6 +257,7 @@
   void visitGraph(HGraph visitee) {
     _graph = visitee;
     visitDominatorTree(visitee);
+    finalizeArrayFlagEffects();
   }
 
   @override
@@ -1618,11 +1621,21 @@
 
     // Can we find the length as an input to an allocation?
     HInstruction potentialAllocation = receiver;
-    if (receiver is HInvokeStatic &&
-        receiver.element == commonElements.setArrayType) {
-      // Look through `setArrayType(new Array(), ...)`
-      potentialAllocation = receiver.inputs.first;
+
+    SCAN:
+    while (!_graph.allocatedFixedLists.contains(potentialAllocation)) {
+      switch (potentialAllocation) {
+        case HInvokeStatic(:final element)
+            when element == commonElements.setArrayType:
+          // Look through `setArrayType(new Array(), ...)`
+          potentialAllocation = potentialAllocation.inputs.first;
+        case HArrayFlagsCheck(:final array) || HArrayFlagsSet(:final array):
+          potentialAllocation = array;
+        default:
+          break SCAN;
+      }
     }
+
     if (_graph.allocatedFixedLists.contains(potentialAllocation)) {
       // TODO(sra): How do we keep this working if we lower/inline the receiver
       // in an optimization?
@@ -2350,7 +2363,7 @@
 
   @override
   HInstruction visitInstanceEnvironment(HInstanceEnvironment node) {
-    HInstruction instance = node.inputs.single;
+    HInstruction instance = node.inputs.single.nonCheck();
 
     // Store-forward instance types of created instances and constant instances.
     //
@@ -2626,6 +2639,105 @@
     }
     return node;
   }
+
+  @override
+  HInstruction visitArrayFlagsCheck(HArrayFlagsCheck node) {
+    // TODO(sra): Implement removal on basis of type, an 'isRedundant' check.
+
+    final array = node.array;
+    final arrayFlags = node.arrayFlags;
+    final checkFlags = node.checkFlags;
+
+    if (arrayFlags is HConstant && arrayFlags.constant.isZero) return array;
+
+    if (array is HArrayFlagsCheck) {
+      // Dependent check. Checks become dependent during types_propagation.
+      if (arrayFlags == array.arrayFlags && checkFlags == array.checkFlags) {
+        // Check is redundant, even if the `node.operation` is different
+        // (different operations are not picked up by GVN).
+        //
+        // TODO(sra): If a stronger check dominates a weaker check (e.g. check
+        // for immutable before check for fixed length), we can match that with
+        // different flags.
+        return array;
+      }
+    }
+
+    return node;
+  }
+
+  /// All HArrayFlagsGet instructions that depend on something. Used to promote
+  /// `HArrayFlagsGet` instructions to side-effect insensitive.  See
+  /// [finalizeArrayFlagEffects] for details.
+  List<HArrayFlagsGet>? _arrayFlagsGets;
+  bool _arrayFlagsEffect = false;
+
+  @override
+  HInstruction visitArrayFlagsSet(HArrayFlagsSet node) {
+    _arrayFlagsEffect = true;
+    return node;
+  }
+
+  @override
+  HInstruction visitArrayFlagsGet(HArrayFlagsGet node) {
+    if (node.sideEffects.dependsOnSomething()) {
+      (_arrayFlagsGets ??= []).add(node);
+    } else {
+      // If the HArrayFlagsGet is pure and the source is visible, then there is
+      // no HArrayFlagsSet instruction that changes the flags, so the flags are
+      // `0`. This can remove checks on allocations in the same method. To do
+      // this for typed arrays, we need to recognize the allocation.
+
+      final array = node.inputs.single;
+
+      if (array is HForeignCode) {
+        final behavior = array.nativeBehavior;
+        if (behavior != null && behavior.isAllocation) {
+          return _graph.addConstantInt(ArrayFlags.none, _closedWorld);
+        }
+      }
+    }
+
+    // The following store-forwarding of the flags is valid only because all
+    // code in the SDK has a 'linear' pattern where the original value is never
+    // accessed after it is 'tagged' with the flags.
+    HInstruction array = node.inputs.single;
+    while (array is HArrayFlagsCheck) {
+      array = array.array;
+    }
+    if (array case HArrayFlagsSet(:final flags)) return flags;
+
+    return node;
+  }
+
+  void finalizeArrayFlagEffects() {
+    // HArrayFlagsGet operations must not be moved past HArrayFlagsSet
+    // operations on the same Array or typed data view. Initially we prevent
+    // this by making HArrayFlagsSet have a changes-property side effect, and
+    // making HArrayFlagsGet depend on that effect.
+    //
+    // This turns out to be rather restrictive and a general 'depends on
+    // property' dependency inhibits important optimizations like hoisting
+    // HArrayFlagsGet out of loops. We could try an add a new effect, but since
+    // the effect analysis is not aware of (non)aliasing, the new effect would
+    // largely have the same problem.
+    //
+    // Instead we notice that HArrayFlagsSet is rare: it is used to implement
+    // constructors that initialize the data, and then mark it as unmodifiable
+    // or fixed-length. If we invoke a callee that does a HArrayFlagsSet
+    // operation, the target of that operation is not visible to the caller.
+    //
+    // Therefore we assume that if we can't see any HArrayFlagsSet operations in
+    // the current method, they cannot change the value observed by
+    // HArrayFlagsGet, and we can pretent the HArrayFlagsGets are pure.
+
+    if (_arrayFlagsGets == null || _arrayFlagsEffect) return;
+
+    for (final instruction in _arrayFlagsGets!) {
+      // Instruction may have been removed from the CFG, but that is harmless.
+      instruction.sideEffects.clearAllDependencies();
+    }
+  }
 }
 
 class SsaDeadCodeEliminator extends HGraphVisitor implements OptimizationPhase {
diff --git a/pkg/compiler/lib/src/ssa/tracer.dart b/pkg/compiler/lib/src/ssa/tracer.dart
index 397f2d5..e0d09f9 100644
--- a/pkg/compiler/lib/src/ssa/tracer.dart
+++ b/pkg/compiler/lib/src/ssa/tracer.dart
@@ -781,4 +781,22 @@
     var inputs = node.inputs.map(temporaryId).join(', ');
     return "TypeBind: $inputs";
   }
+
+  @override
+  String visitArrayFlagsCheck(HArrayFlagsCheck node) {
+    var inputs = node.inputs.map(temporaryId).join(', ');
+    return "ArrayFlagsCheck: $inputs";
+  }
+
+  @override
+  String visitArrayFlagsGet(HArrayFlagsGet node) {
+    var inputs = node.inputs.map(temporaryId).join(', ');
+    return "ArrayFlagsGet: $inputs";
+  }
+
+  @override
+  String visitArrayFlagsSet(HArrayFlagsSet node) {
+    var inputs = node.inputs.map(temporaryId).join(', ');
+    return "ArrayFlagsSet: $inputs";
+  }
 }
diff --git a/pkg/compiler/lib/src/ssa/types_propagation.dart b/pkg/compiler/lib/src/ssa/types_propagation.dart
index 11569df..082036c 100644
--- a/pkg/compiler/lib/src/ssa/types_propagation.dart
+++ b/pkg/compiler/lib/src/ssa/types_propagation.dart
@@ -503,4 +503,12 @@
     return abstractValueDomain.intersection(
         abstractValueDomain.boolType, instruction.checkedInput.instructionType);
   }
+
+  @override
+  AbstractValue visitArrayFlagsCheck(HArrayFlagsCheck instruction) {
+    instruction.array
+        .replaceAllUsersDominatedBy(instruction.next!, instruction);
+    AbstractValue inputType = instruction.array.instructionType;
+    return instruction.computeInstructionType(inputType, abstractValueDomain);
+  }
 }
diff --git a/pkg/compiler/lib/src/ssa/variable_allocator.dart b/pkg/compiler/lib/src/ssa/variable_allocator.dart
index 895e628..d1457e8 100644
--- a/pkg/compiler/lib/src/ssa/variable_allocator.dart
+++ b/pkg/compiler/lib/src/ssa/variable_allocator.dart
@@ -225,14 +225,15 @@
   // not on the checked instruction t1.
   // When looking for the checkedInstructionOrNonGenerateAtUseSite of t3 we must
   // return t2.
-  HInstruction checkedInstructionOrNonGenerateAtUseSite(HCheck check) {
-    HInstruction checked = check.checkedInput;
-    while (checked is HCheck) {
-      HInstruction next = checked.checkedInput;
+  HInstruction checkedInstructionOrNonGenerateAtUseSite(
+      HOutputConstrainedToAnInput check) {
+    HInstruction constraint = check.constrainedInput;
+    while (constraint is HOutputConstrainedToAnInput) {
+      HInstruction next = constraint.constrainedInput;
       if (generateAtUseSite.contains(next)) break;
-      checked = next;
+      constraint = next;
     }
-    return checked;
+    return constraint;
   }
 
   void markAsLiveInEnvironment(
@@ -244,11 +245,11 @@
       // Special case the HCheck instruction to mark the actual
       // checked instruction live. The checked instruction and the
       // [HCheck] will share the same live ranges.
-      if (instruction is HCheck) {
-        HCheck check = instruction;
-        HInstruction checked = checkedInstructionOrNonGenerateAtUseSite(check);
-        if (!generateAtUseSite.contains(checked)) {
-          environment.add(checked, instructionId);
+      if (instruction is HOutputConstrainedToAnInput) {
+        HInstruction constraint =
+            checkedInstructionOrNonGenerateAtUseSite(instruction);
+        if (!generateAtUseSite.contains(constraint)) {
+          environment.add(constraint, instructionId);
         }
       }
     }
@@ -259,15 +260,15 @@
     environment.remove(instruction, instructionId);
     // Special case the HCheck instruction to have the same live
     // interval as the instruction it is checking.
-    if (instruction is HCheck) {
-      HCheck check = instruction;
-      HInstruction checked = checkedInstructionOrNonGenerateAtUseSite(check);
-      if (!generateAtUseSite.contains(checked)) {
-        liveIntervals.putIfAbsent(checked, () => LiveInterval());
+    if (instruction is HOutputConstrainedToAnInput) {
+      HInstruction constraint =
+          checkedInstructionOrNonGenerateAtUseSite(instruction);
+      if (!generateAtUseSite.contains(constraint)) {
+        liveIntervals.putIfAbsent(constraint, () => LiveInterval());
         // Unconditionally force the live ranges of the HCheck to
         // be the live ranges of the instruction it is checking.
         liveIntervals[instruction] =
-            LiveInterval.forCheck(instructionId, liveIntervals[checked]!);
+            LiveInterval.forCheck(instructionId, liveIntervals[constraint]!);
       }
     }
   }
@@ -493,14 +494,14 @@
 
   String allocateName(HInstruction instruction) {
     String? name;
-    if (instruction is HCheck) {
-      // Special case this instruction to use the name of its
-      // input if it has one.
+    if (instruction is HOutputConstrainedToAnInput) {
+      // Special case this instruction to use the name of its input if it has
+      // one.
       HInstruction temp = instruction;
       do {
-        temp = (temp as HCheck).checkedInput;
+        temp = (temp as HOutputConstrainedToAnInput).constrainedInput;
         name = names.ownName[temp];
-      } while (name == null && temp is HCheck);
+      } while (name == null && temp is HOutputConstrainedToAnInput);
       if (name != null) return addAllocatedName(instruction, name);
     }
 
diff --git a/pkg/compiler/test/codegen/data/unmodifiable_bytedata.dart b/pkg/compiler/test/codegen/data/unmodifiable_bytedata.dart
new file mode 100644
index 0000000..84b3054
--- /dev/null
+++ b/pkg/compiler/test/codegen/data/unmodifiable_bytedata.dart
@@ -0,0 +1,117 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:_foreign_helper' show ArrayFlags, HArrayFlagsSet;
+
+import 'dart:typed_data' show ByteData, Endian;
+
+@pragma('dart2js:never-inline')
+/*member: returnUnmodifiable:function() {
+  var a = new DataView(new ArrayBuffer(10));
+  a.$flags = 3;
+  return a;
+}*/
+returnUnmodifiable() {
+  final a = ByteData(10);
+  ByteData b = HArrayFlagsSet(a, ArrayFlags.unmodifiable);
+  return b;
+}
+
+// Two writes, neither checked.
+@pragma('dart2js:never-inline')
+/*member: returnModifiable1:function() {
+  var data = new DataView(new ArrayBuffer(10));
+  data.setInt16(0, 200, false);
+  data.setInt32(4, 200, false);
+  return data;
+}*/
+returnModifiable1() {
+  final data = ByteData(10);
+  data.setInt16(0, 200);
+  data.setInt32(4, 200);
+  return data;
+}
+
+// Two writes, neither checked.
+@pragma('dart2js:never-inline')
+/*member: returnModifiable2:function() {
+  var data = new DataView(new ArrayBuffer(10));
+  data.setInt16(0, 200, false);
+  data.setInt32(4, 200, false);
+  return data;
+}*/
+returnModifiable2() {
+  final data = ByteData(10);
+  data.setInt16(0, 200);
+  data.setInt32(4, 200);
+  return data;
+}
+
+@pragma('dart2js:never-inline')
+/*member: guaranteedFail:function() {
+  var a = new DataView(new ArrayBuffer(10));
+  a.$flags = 3;
+  A.throwUnsupportedOperation(a, "setInt32");
+  a.setInt32(0, 100, false);
+  a.setUint32(4, 2000, false);
+  return a;
+}*/
+guaranteedFail() {
+  final a = ByteData(10);
+  ByteData b = HArrayFlagsSet(a, ArrayFlags.unmodifiable);
+  b.setInt32(0, 100);
+  b.setUint32(4, 2000);
+  return b;
+}
+
+@pragma('dart2js:never-inline')
+/*member: multipleWrites:function(data) {
+  data.$flags & 2 && A.throwUnsupportedOperation(data, "setFloat64");
+  data.setFloat64(0, 1.23, false);
+  data.setFloat32(8, 1.23, false);
+  return data;
+}*/
+multipleWrites(ByteData data) {
+  // there should only be one write check.
+  data.setFloat64(0, 1.23);
+  data.setFloat32(8, 1.23);
+  return data;
+}
+
+@pragma('dart2js:never-inline')
+/*member: hoistedLoad:function(data) {
+  var t1, i;
+  for (t1 = data.$flags | 0, i = 0; i < data.byteLength; i += 2) {
+    t1 & 2 && A.throwUnsupportedOperation(data, "setUint16");
+    data.setUint16(i, 100, true);
+  }
+  return data;
+}*/
+ByteData hoistedLoad(ByteData data) {
+  // The load of the flags is hoisted, but the check is not.
+  for (int i = 0; i < data.lengthInBytes; i += 2) {
+    data.setUint16(i, 100, Endian.little);
+  }
+  return data;
+}
+
+@pragma('dart2js:never-inline')
+/*member: maybeUnmodifiable:ignore*/
+ByteData maybeUnmodifiable() {
+  var data = ByteData(100);
+  if (DateTime.now().millisecondsSinceEpoch == 42) {
+    data = data.asUnmodifiableView();
+  }
+  return data;
+}
+
+/*member: main:ignore*/
+main() {
+  print(returnUnmodifiable());
+  print(returnModifiable1());
+  print(returnModifiable2());
+  print(guaranteedFail);
+  print(multipleWrites(maybeUnmodifiable()));
+  print(hoistedLoad(maybeUnmodifiable()));
+}
diff --git a/pkg/compiler/test/codegen/data/unmodifiable_list.dart b/pkg/compiler/test/codegen/data/unmodifiable_list.dart
new file mode 100644
index 0000000..caca8fa
--- /dev/null
+++ b/pkg/compiler/test/codegen/data/unmodifiable_list.dart
@@ -0,0 +1,50 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@pragma('dart2js:never-inline')
+/*spec|canary.member: test1:function(a) {
+  B.JSArray_methods.$indexSet(a, 5, 1);
+  B.JSArray_methods.$indexSet(a, 0, 2);
+  return a;
+}*/
+/*prod.member: test1:function(a) {
+  a.$flags & 2 && A.throwUnsupportedOperation(a);
+  if (5 >= a.length)
+    return A.ioore(a, 5);
+  a[5] = 1;
+  a[0] = 2;
+  return a;
+}*/
+List<int> test1(List<int> a) {
+  a[5] = 1;
+  a[0] = 2;
+  return a;
+}
+
+@pragma('dart2js:never-inline')
+/*member: test2:function(a) {
+  B.JSArray_methods.add$1(a, 100);
+  return a;
+}*/
+List<int> test2(List<int> a) {
+  a.add(100);
+  return a;
+}
+
+@pragma('dart2js:never-inline')
+bool isEven(int i) => i.isEven;
+
+@pragma('dart2js:never-inline')
+/*member: maybeUnmodifiable:ignore*/
+List<int> maybeUnmodifiable() {
+  List<int> d = List.filled(10, 0);
+  if (DateTime.now().millisecondsSinceEpoch == 42) d = List.unmodifiable(d);
+  return d;
+}
+
+/*member: main:ignore*/
+main() {
+  print(test1(maybeUnmodifiable()));
+  print(test2(maybeUnmodifiable()));
+}
diff --git a/pkg/compiler/test/codegen/data/unmodifiable_typed_list.dart b/pkg/compiler/test/codegen/data/unmodifiable_typed_list.dart
new file mode 100644
index 0000000..61a9ed8
--- /dev/null
+++ b/pkg/compiler/test/codegen/data/unmodifiable_typed_list.dart
@@ -0,0 +1,196 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:_foreign_helper' show ArrayFlags, HArrayFlagsSet;
+
+import 'dart:typed_data';
+
+@pragma('dart2js:never-inline')
+/*member: returnUnmodifiable:function() {
+  var a = new Int8Array(10);
+  a.$flags = 3;
+  return a;
+}*/
+returnUnmodifiable() {
+  final a = Int8List(10);
+  Int8List b = HArrayFlagsSet(a, ArrayFlags.unmodifiable);
+  return b;
+}
+
+// Two writes, neither checked.
+@pragma('dart2js:never-inline')
+/*member: return200:function() {
+  var a = new Uint8Array(10);
+  a[0] = 200;
+  a[1] = 201;
+  return a;
+}*/
+return200() {
+  final a = Uint8List(10);
+  a[0] = 200;
+  a[1] = 201;
+  return a;
+}
+
+@pragma('dart2js:never-inline')
+/*member: guaranteedFail:function() {
+  var a = new Uint8Array(10);
+  a.$flags = 3;
+  A.throwUnsupportedOperation(a);
+  a[0] = 200;
+  a[1] = 201;
+  return a;
+}*/
+guaranteedFail() {
+  final a = Uint8List(10);
+  Uint8List b = HArrayFlagsSet(a, ArrayFlags.unmodifiable);
+  b[0] = 200;
+  b[1] = 201;
+  return b;
+}
+
+@pragma('dart2js:never-inline')
+/*member: multipleWrites:function() {
+  var a = A.maybeUnmodifiable();
+  a.$flags & 2 && A.throwUnsupportedOperation(a);
+  if (5 >= a.length)
+    return A.ioore(a, 5);
+  a[5] = 100;
+  a[1] = 200;
+  return a;
+}*/
+multipleWrites() {
+  final a = maybeUnmodifiable();
+  a[5] = 100;
+  a[1] = 200; // there should only be one write check.
+  return a;
+}
+
+@pragma('dart2js:never-inline')
+/*member: hoistedLoad:function(a) {
+  var t1, t2, i;
+  for (t1 = a.length, t2 = a.$flags | 0, i = 0; i < t1; ++i) {
+    t2 & 2 && A.throwUnsupportedOperation(a);
+    a[i] = 100;
+  }
+  return a;
+}*/
+Uint8List hoistedLoad(Uint8List a) {
+  // The load of the flags is hoisted, but the check is not.
+  for (int i = 0; i < a.length; i++) {
+    a[i] = 100;
+  }
+  return a;
+}
+
+@pragma('dart2js:never-inline')
+/*member: hoistedCheck2:function(a) {
+  var t2, i,
+    t1 = a.length;
+  if (t1 > 0)
+    for (t2 = a.$flags | 0, i = 0; i < t1; ++i) {
+      t2 & 2 && A.throwUnsupportedOperation(a);
+      a[i] = 100;
+    }
+  return a;
+}*/
+Uint8List hoistedCheck2(Uint8List a) {
+  if (a.length > 0) {
+    // We should be able to do better here - the loop has a non-zero minimum
+    // trip count.
+    for (int i = 0; i < a.length; i++) {
+      a[i] = 100;
+    }
+  }
+  return a;
+}
+
+@pragma('dart2js:never-inline')
+/*spec|canary.member: hoistedCheck3:function(a) {
+  var t2, i,
+    t1 = a.length;
+  if (t1 > 0) {
+    a.$flags & 2 && A.throwUnsupportedOperation(a);
+    a[0] = 100;
+    for (t2 = a.$flags | 0, i = 1; i < t1; ++i) {
+      t2 & 2 && A.throwUnsupportedOperation(a);
+      a[i] = 100;
+    }
+  }
+  return a;
+}*/
+/*prod.member: hoistedCheck3:function(a) {
+  var i,
+    t1 = a.length;
+  if (t1 > 0) {
+    a.$flags & 2 && A.throwUnsupportedOperation(a);
+    a[0] = 100;
+    for (i = 1; i < t1; ++i)
+      a[i] = 100;
+  }
+  return a;
+}*/
+Uint8List hoistedCheck3(Uint8List a) {
+  if (a.length > 0) {
+    a[0] = 100;
+    // Checks in the loop are removed via simple dominance.
+    for (int i = 1; i < a.length; i++) {
+      a[i] = 100;
+    }
+  }
+  return a;
+}
+
+@pragma('dart2js:never-inline')
+/*spec|canary.member: list1:function(a) {
+  B.JSArray_methods.$indexSet(a, 1, 100);
+  B.JSArray_methods.$indexSet(a, 2, 200);
+  return a;
+}*/
+/*prod.member: list1:function(a) {
+  var t1;
+  a.$flags & 2 && A.throwUnsupportedOperation(a);
+  t1 = a.length;
+  if (1 >= t1)
+    return A.ioore(a, 1);
+  a[1] = 100;
+  if (2 >= t1)
+    return A.ioore(a, 2);
+  a[2] = 200;
+  return a;
+}*/
+List<int> list1(List<int> a) {
+  a[1] = 100;
+  a[2] = 200;
+  return a;
+}
+
+@pragma('dart2js:never-inline')
+/*member: maybeUnmodifiable:ignore*/
+Uint8List maybeUnmodifiable() {
+  var d = Uint8List(10);
+  if (DateTime.now().millisecondsSinceEpoch == 42) d = d.asUnmodifiableView();
+  return d;
+}
+
+@pragma('dart2js:never-inline')
+/*member: maybeUnmodifiableList:ignore*/
+List<int> maybeUnmodifiableList() {
+  var d = List<int>.filled(10, 0);
+  if (DateTime.now().millisecondsSinceEpoch == 42) d = List.unmodifiable(d);
+  return d;
+}
+
+/*member: main:ignore*/
+main() {
+  print(returnUnmodifiable());
+  print(return200());
+  print(guaranteedFail);
+  print(multipleWrites());
+  print(hoistedLoad(maybeUnmodifiable()));
+  print(hoistedCheck2(maybeUnmodifiable()));
+  print(hoistedCheck3(maybeUnmodifiable()));
+
+  print(list1(maybeUnmodifiableList()));
+}
diff --git a/pkg/compiler/test/optimization/data/index_assign.dart b/pkg/compiler/test/optimization/data/index_assign.dart
index 28a23bb..3b76788 100644
--- a/pkg/compiler/test/optimization/data/index_assign.dart
+++ b/pkg/compiler/test/optimization/data/index_assign.dart
@@ -47,7 +47,8 @@
   list[0] = value;
 }
 
-/*member: immutableListIndexAssign:Specializer=[!IndexAssign]*/
+/*spec.member: immutableListIndexAssign:Specializer=[!IndexAssign]*/
+/*prod.member: immutableListIndexAssign:Specializer=[IndexAssign]*/
 @pragma('dart2js:noInline')
 immutableListIndexAssign() {
   var list = const [0];
diff --git a/pkg/compiler/test/rti/data/generic_methods_dynamic_05.dart b/pkg/compiler/test/rti/data/generic_methods_dynamic_05.dart
index 416d81b..2fe2fdb 100644
--- a/pkg/compiler/test/rti/data/generic_methods_dynamic_05.dart
+++ b/pkg/compiler/test/rti/data/generic_methods_dynamic_05.dart
@@ -6,11 +6,11 @@
 
 // Test derived from language/generic_methods_dynamic_test/05
 
-/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
+/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
 /*prod.class: global#JSArray:deps=[List],implicit=[JSArray.E],needsArgs,test*/
 
-/*spec.class: global#List:deps=[C.bar,JSArray.markFixedList],explicit=[List<B>,List<Object>,List<Object?>,List<String>?,List<markFixedList.T>],needsArgs,test*/
-/*prod.class: global#List:deps=[C.bar],explicit=[List<B>],needsArgs,test*/
+/*spec.class: global#List:deps=[C.bar,JSArray.markFixedList],explicit=[List,List<B>,List<Object>,List<Object?>,List<String>?,List<markFixedList.T>],needsArgs,test*/
+/*prod.class: global#List:deps=[C.bar],explicit=[List,List<B>],needsArgs,test*/
 
 class A {}
 
diff --git a/pkg/compiler/test/rti/data/list_literal.dart b/pkg/compiler/test/rti/data/list_literal.dart
index a7d1ed6..58e938f 100644
--- a/pkg/compiler/test/rti/data/list_literal.dart
+++ b/pkg/compiler/test/rti/data/list_literal.dart
@@ -3,9 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /*spec.class: global#List:deps=[Class.m,JSArray.markFixedList],explicit=[List,List<Object>,List<Object?>,List<String>?,List<markFixedList.T>],needsArgs,test*/
-/*prod.class: global#List:deps=[Class.m],needsArgs,test*/
+/*prod.class: global#List:deps=[Class.m],explicit=[List],needsArgs,test*/
 
-/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
+/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
 /*prod.class: global#JSArray:deps=[List],implicit=[JSArray.E],needsArgs,test*/
 
 main() {
diff --git a/pkg/compiler/test/rti/data/list_to_set.dart b/pkg/compiler/test/rti/data/list_to_set.dart
index 1628fdd..646d0f3 100644
--- a/pkg/compiler/test/rti/data/list_to_set.dart
+++ b/pkg/compiler/test/rti/data/list_to_set.dart
@@ -3,9 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /*spec.class: global#List:deps=[Class,JSArray.markFixedList],explicit=[List,List<Object>,List<Object?>,List<String>?,List<markFixedList.T>],needsArgs,test*/
-/*prod.class: global#List:deps=[Class],needsArgs,test*/
+/*prod.class: global#List:deps=[Class],explicit=[List],needsArgs,test*/
 
-/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
+/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
 /*prod.class: global#JSArray:deps=[List],implicit=[JSArray.E],needsArgs,test*/
 
 main() {
diff --git a/pkg/compiler/test/rti/data/local_function_list_literal.dart b/pkg/compiler/test/rti/data/local_function_list_literal.dart
index 9b3c422..0dd6869 100644
--- a/pkg/compiler/test/rti/data/local_function_list_literal.dart
+++ b/pkg/compiler/test/rti/data/local_function_list_literal.dart
@@ -4,7 +4,7 @@
 
 import 'package:compiler/src/util/testing.dart';
 
-/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
+/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
 /*prod.class: global#JSArray:deps=[List],implicit=[JSArray.E],needsArgs,test*/
 
 @pragma('dart2js:noInline')
diff --git a/pkg/js_runtime/lib/synced/array_flags.dart b/pkg/js_runtime/lib/synced/array_flags.dart
new file mode 100644
index 0000000..d8ca41e
--- /dev/null
+++ b/pkg/js_runtime/lib/synced/array_flags.dart
@@ -0,0 +1,63 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Values for the 'array flags' property that is attached to native JSArray and
+/// typed data objects to encode restrictions.
+///
+/// We encode the restrictions as two bits - one for potentially growable
+/// objects that are restricted to be fixed length, and a second for objects
+/// with potentially modifiable contents that are restricted to have
+/// unmodifiable contents. We only use three combinations (00 = growable, 01 =
+/// fixed-length, 11 = unmodifiable), since the core List class does not have a
+/// variant that is an 'append-only' list (would be 10).
+///
+/// We use the same scheme for typed data so that the same check works for
+/// modifiability checks for `a[i] = v` when `a` is a JSarray or Uint8Array.
+///
+/// `const` JSArray values are tagged with an additional bit for future use in
+/// avoiding copying. We could also use the `const` bit for unmodifiable typed
+/// data for which there in no modifable view of the underlying buffer.
+///
+/// Given the absense of append-only lists, we could have used a more
+/// numerically compact scheme (0 = growable, 1 = fixed-length, 2 =
+/// unmodifiable, 3 = const). This compact scheme requires comparison
+///
+///     if (x.flags > 0)
+///
+/// rather than mask-testing
+///
+///     if (x.flags & 1)
+///
+/// The unrestricted flags (0) are usually encoded as a missing property, i.e.,
+/// `undefined`, which is converted to NaN for '>', but converted to the more
+/// comfortable '0' for '&'. We hope that there is less to go potentially go
+/// wrong with JavaScript engine slow paths with the bitmask.
+class ArrayFlags {
+  /// Value of array flags that marks a JSArray as being fixed-length. This is
+  /// not used on typed data since that is always fixed-length.
+  static const int fixedLength = fixedLengthCheck;
+
+  /// Value of array flags that marks a JSArray or typed-data object as
+  /// unmodifiable. Includes the 'fixed-length' bit, since all unmodifiable
+  /// lists are also fixed length.
+  static const int unmodifiable = fixedLengthCheck | unmodifiableCheck;
+
+  /// Value of array flags that marks a JSArray as a constant (transitively
+  /// unmodifiable).
+  static const int constant =
+      fixedLengthCheck | unmodifiableCheck | constantCheck;
+
+  /// Default value of array flags when there is no flags property. This value
+  /// is not stored on the flags property.
+  static const int none = 0;
+
+  /// Bit to check for fixed-length JSArray.
+  static const int fixedLengthCheck = 1;
+
+  /// Bit to check for unmodifiable JSArray and typed data.
+  static const int unmodifiableCheck = 2;
+
+  /// Bit to check for constant JSArray.
+  static const int constantCheck = 4;
+}
diff --git a/sdk/lib/_internal/js_runtime/lib/foreign_helper.dart b/sdk/lib/_internal/js_runtime/lib/foreign_helper.dart
index 75772bd..6d01aab 100644
--- a/sdk/lib/_internal/js_runtime/lib/foreign_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/foreign_helper.dart
@@ -6,6 +6,9 @@
 
 import 'dart:_js_shared_embedded_names' show JsGetName, JsBuiltin;
 import 'dart:_rti' show Rti;
+import 'dart:_js_helper' show throwUnsupportedOperation;
+
+export 'dart:_array_flags';
 
 /// Emits a JavaScript code fragment parametrized by arguments.
 ///
@@ -300,3 +303,57 @@
 /// Returns `true` if [value] is the sentinel JavaScript value created through
 /// [createJsSentinel].
 external bool isJsSentinel(dynamic value);
+
+/// Reads the permission flags from a JSArray, typed data array or typed data
+/// ByteData.
+///
+/// Corresponds to the SSA instruction with the same name.
+external int HArrayFlagsGet(Object array);
+
+/// Checks the 'array flags' of [array] for permission to modify or grow the
+/// array or typed data. Throws if the [arrayFlags] do not permit the
+/// operation(s) specified by [checkFlags].  Returns `array`, possibly with a
+/// different type that reflects the checked flags.
+///
+/// Corresponds to the SSA instruction with the same name.
+///
+/// The following is a typical use to check modifiability for `a[i] = 0`. The
+/// array flags are checked to see if they prohibit modification.
+///
+///     a = ...
+///     f = HArrayFlagsGet(a);
+///     a2 = HArrayFlagsCheck(a, f, ArrayFlags.unmodifiableCheck, "[]=")
+///     a2[i] = 0;
+///
+/// It is critical that the modifying operation uses the value returned by
+/// HArrayFlagsCheck and not the argument. This data dependency is how we
+/// prevent the optimizer from moving the modifying operation before the check.
+///
+/// Code should be written with a HArrayFlagsGet / HArrayFlagsCheck pair
+/// immediately before each modifying operation. The optimizer will remove
+/// unnecessary instructions, but to do so correctly, it requires the Get/Check
+/// to be initially in a position where there can be no way for there to be an
+/// intervening HArrayFlagsSet that might invalidate the check.
+@pragma('dart2js:as:trust')
+T HArrayFlagsCheck<T>(
+    Object array, int arrayFlags, int checkFlags, String operation) {
+  // This body is unused but serves as a model for global for impacts and
+  // analysis.
+  if (arrayFlags & checkFlags != 0) {
+    throwUnsupportedOperation(array, operation);
+  }
+  return array as T;
+}
+
+/// Sets the permission flags on a JSArray or typed data object. Returns
+/// `array`, possibly with a different type that reflects the changed flags.
+///
+/// Corresponds to the SSA instruction with the same name.
+///
+/// Care must be taken when combining HArrayFlagsSet with HArrayFlagsGet and
+/// HArrayFlagsCheck. In order to ensure that the most recent version of the
+/// flags is used, code must be written in a 'linear' style where the input to
+/// HArrayFlagsSet is not used afterwards, only the result. One coding style
+/// that ensures a linear pattern is to return the HArrayFlagsSet of something
+/// that is allocated within the function and not otherwise stored.
+external T HArrayFlagsSet<T>(Object array, int flags);
diff --git a/sdk/lib/_internal/js_runtime/lib/interceptors.dart b/sdk/lib/_internal/js_runtime/lib/interceptors.dart
index f430efa..71fa151 100644
--- a/sdk/lib/_internal/js_runtime/lib/interceptors.dart
+++ b/sdk/lib/_internal/js_runtime/lib/interceptors.dart
@@ -8,7 +8,15 @@
     show DISPATCH_PROPERTY_NAME, TYPE_TO_INTERCEPTOR_MAP;
 
 import 'dart:collection' hide LinkedList, LinkedListEntry;
-import 'dart:_foreign_helper' show JS_FALSE, JS_GET_FLAG, TYPE_REF;
+import 'dart:_foreign_helper'
+    show
+        JS_FALSE,
+        JS_GET_FLAG,
+        TYPE_REF,
+        ArrayFlags,
+        HArrayFlagsGet,
+        HArrayFlagsSet,
+        HArrayFlagsCheck;
 import 'dart:_internal' hide Symbol;
 import "dart:_internal" as _symbol_dev show Symbol;
 import 'dart:_js_helper'
diff --git a/sdk/lib/_internal/js_runtime/lib/js_array.dart b/sdk/lib/_internal/js_runtime/lib/js_array.dart
index 00d4fe0..03ccb14 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_array.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_array.dart
@@ -36,7 +36,7 @@
     if (length < 0 || length > maxJSArrayLength) {
       throw RangeError.range(length, 0, maxJSArrayLength, 'length');
     }
-    return JSArray<E>.markFixed(JS('', 'new Array(#)', length));
+    return JSArray<E>.markFixed(JS('JSArray', 'new Array(#)', length));
   }
 
   /// Returns a fresh JavaScript Array, marked as fixed-length.  The Array is
@@ -61,7 +61,7 @@
     if (length < 0 || length > maxJSArrayLength) {
       throw RangeError.range(length, 0, maxJSArrayLength, 'length');
     }
-    return JSArray<E>.markFixed(JS('', 'new Array(#)', length));
+    return JSArray<E>.markFixed(JS('JSArray', 'new Array(#)', length));
   }
 
   /// Returns a fresh growable JavaScript Array of zero length length.
@@ -78,7 +78,7 @@
     if ((length is! int) || (length < 0)) {
       throw ArgumentError('Length must be a non-negative integer: $length');
     }
-    return JSArray<E>.markGrowable(JS('', 'new Array(#)', length));
+    return JSArray<E>.markGrowable(JS('JSArray', 'new Array(#)', length));
   }
 
   /// Returns a fresh growable JavaScript Array with initial length. The Array
@@ -95,7 +95,7 @@
     if ((length is! int) || (length < 0)) {
       throw ArgumentError('Length must be a non-negative integer: $length');
     }
-    return JSArray<E>.markGrowable(JS('', 'new Array(#)', length));
+    return JSArray<E>.markGrowable(JS('JSArray', 'new Array(#)', length));
   }
 
   /// Constructor for adding type parameters to an existing JavaScript Array.
@@ -118,29 +118,24 @@
   factory JSArray.markGrowable(allocation) =>
       JS('JSExtendableArray', '#', JSArray<E>.typed(allocation));
 
+  @pragma('dart2js:prefer-inline')
   static List<T> markFixedList<T>(List<T> list) {
-    // Functions are stored in the hidden class and not as properties in
-    // the object. We never actually look at the value, but only want
-    // to know if the property exists.
-    JS('void', r'#.fixed$length = Array', list);
-    return JS('JSFixedArray', '#', list);
+    return JS(
+        'JSFixedArray', '#', HArrayFlagsSet(list, ArrayFlags.fixedLength));
   }
 
+  @pragma('dart2js:prefer-inline')
   static List<T> markUnmodifiableList<T>(List list) {
-    // Functions are stored in the hidden class and not as properties in
-    // the object. We never actually look at the value, but only want
-    // to know if the property exists.
-    JS('void', r'#.fixed$length = Array', list);
-    JS('void', r'#.immutable$list = Array', list);
-    return JS('JSUnmodifiableArray', '#', list);
+    return JS('JSUnmodifiableArray', '#',
+        HArrayFlagsSet(list, ArrayFlags.unmodifiable));
   }
 
   static bool isFixedLength(JSArray a) {
-    return !JS('bool', r'!#.fixed$length', a);
+    return HArrayFlagsGet(a) & ArrayFlags.fixedLengthCheck != 0;
   }
 
   static bool isUnmodifiable(JSArray a) {
-    return !JS('bool', r'!#.immutable$list', a);
+    return HArrayFlagsGet(a) & ArrayFlags.unmodifiableCheck != 0;
   }
 
   static bool isGrowable(JSArray a) {
@@ -152,15 +147,13 @@
   }
 
   checkMutable(String reason) {
-    if (!isMutable(this)) {
-      throw UnsupportedError(reason);
-    }
+    final int flags = HArrayFlagsGet(this);
+    HArrayFlagsCheck(this, flags, ArrayFlags.unmodifiableCheck, reason);
   }
 
   checkGrowable(String reason) {
-    if (!isGrowable(this)) {
-      throw UnsupportedError(reason);
-    }
+    final int flags = HArrayFlagsGet(this);
+    HArrayFlagsCheck(this, flags, ArrayFlags.fixedLengthCheck, reason);
   }
 
   List<R> cast<R>() => List.castFrom<E, R>(this);
@@ -790,11 +783,14 @@
   }
 
   void operator []=(int index, E value) {
-    checkMutable('indexed set');
+    final int flags = HArrayFlagsGet(this);
+    final checked =
+        HArrayFlagsCheck(this, flags, ArrayFlags.unmodifiableCheck, '[]=');
+
     if (index is! int) throw diagnoseIndexError(this, index);
     // This form of the range test correctly rejects NaN.
     if (!(index >= 0 && index < length)) throw diagnoseIndexError(this, index);
-    JS('void', r'#[#] = #', this, index, value);
+    JS('void', r'#[#] = #', checked, index, value);
   }
 
   Map<int, E> asMap() {
diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
index 6f15c9e..13d35a8 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
@@ -36,6 +36,8 @@
     show
         DART_CLOSURE_TO_JS,
         getInterceptor,
+        ArrayFlags,
+        HArrayFlagsGet,
         JS,
         JS_BUILTIN,
         JS_CONST,
@@ -1249,6 +1251,47 @@
   throw UnsupportedError(message);
 }
 
+/// Called from code generated for the HArrayFlagsCheck instruction.
+///
+/// A missing `operation` argument defaults to '[]='.
+Never throwUnsupportedOperation(Object o, [String? operation]) {
+  // Missing argument is defaulted manually.  The calling convention for
+  // top-level methods is that the call site provides the default values. Since
+  // the generated code omits the second argument, `undefined` is passed, which
+  // presents as Dart `null`.
+  operation ??= '[]=';
+  final wrapper = JS('', 'Error()');
+  throwExpressionWithWrapper(
+      _diagnoseUnsupportedOperation(o, operation), wrapper);
+}
+
+Error _diagnoseUnsupportedOperation(Object o, String operation) {
+  String? verb;
+  String adjective = '';
+  String article = 'a';
+  String object;
+  if (o is List) {
+    object = 'list';
+    // TODO(sra): Should we do more of this?
+    if (operation == 'add' || operation == 'addAll') verb ??= 'add to';
+  } else {
+    object = 'ByteData';
+    verb ??= "'$operation' on";
+  }
+  verb ??= 'modify';
+  final flags = HArrayFlagsGet(o);
+  if (flags & ArrayFlags.unmodifiableCheck != 0) {
+    article = 'an ';
+    adjective = 'unmodifiable ';
+  } else {
+    // No need to test for fixed-length, otherwise we would not be diagnosing
+    // the error.
+    article = 'a ';
+    adjective = 'fixed-length ';
+  }
+  return UnsupportedError('Cannot $verb $article$adjective$object');
+}
+
 // This is used in open coded for-in loops on arrays.
 //
 //     checkConcurrentModificationError(a.length == startLength, a)
diff --git a/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart b/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart
index 1fb2edb..dd3f016 100644
--- a/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart
+++ b/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart
@@ -8,7 +8,7 @@
 
 import 'dart:collection' show ListMixin;
 import 'dart:_internal' show FixedLengthListMixin hide Symbol;
-import "dart:_internal" show UnmodifiableListBase;
+import "dart:_internal" show UnmodifiableListMixin;
 import 'dart:_interceptors'
     show JavaScriptObject, JSIndexable, JSUInt32, JSUInt31;
 import 'dart:_js_helper'
@@ -22,7 +22,9 @@
         diagnoseIndexError,
         diagnoseRangeError,
         TrustedGetRuntimeType;
-import 'dart:_foreign_helper' show JS;
+
+import 'dart:_foreign_helper'
+    show JS, ArrayFlags, HArrayFlagsCheck, HArrayFlagsGet, HArrayFlagsSet;
 
 import 'dart:math' as Math;
 
@@ -36,31 +38,32 @@
 
   Type get runtimeType => ByteBuffer;
 
-  Uint8List asUint8List([int offsetInBytes = 0, int? length]) {
+  NativeUint8List asUint8List([int offsetInBytes = 0, int? length]) {
     return NativeUint8List.view(this, offsetInBytes, length);
   }
 
-  Int8List asInt8List([int offsetInBytes = 0, int? length]) {
+  NativeInt8List asInt8List([int offsetInBytes = 0, int? length]) {
     return NativeInt8List.view(this, offsetInBytes, length);
   }
 
-  Uint8ClampedList asUint8ClampedList([int offsetInBytes = 0, int? length]) {
+  NativeUint8ClampedList asUint8ClampedList(
+      [int offsetInBytes = 0, int? length]) {
     return NativeUint8ClampedList.view(this, offsetInBytes, length);
   }
 
-  Uint16List asUint16List([int offsetInBytes = 0, int? length]) {
+  NativeUint16List asUint16List([int offsetInBytes = 0, int? length]) {
     return NativeUint16List.view(this, offsetInBytes, length);
   }
 
-  Int16List asInt16List([int offsetInBytes = 0, int? length]) {
+  NativeInt16List asInt16List([int offsetInBytes = 0, int? length]) {
     return NativeInt16List.view(this, offsetInBytes, length);
   }
 
-  Uint32List asUint32List([int offsetInBytes = 0, int? length]) {
+  NativeUint32List asUint32List([int offsetInBytes = 0, int? length]) {
     return NativeUint32List.view(this, offsetInBytes, length);
   }
 
-  Int32List asInt32List([int offsetInBytes = 0, int? length]) {
+  NativeInt32List asInt32List([int offsetInBytes = 0, int? length]) {
     return NativeInt32List.view(this, offsetInBytes, length);
   }
 
@@ -72,33 +75,33 @@
     throw UnsupportedError('Int64List not supported by dart2js.');
   }
 
-  Int32x4List asInt32x4List([int offsetInBytes = 0, int? length]) {
+  NativeInt32x4List asInt32x4List([int offsetInBytes = 0, int? length]) {
     length ??= (lengthInBytes - offsetInBytes) ~/ Int32x4List.bytesPerElement;
     var storage = this.asInt32List(offsetInBytes, length * 4);
     return NativeInt32x4List._externalStorage(storage);
   }
 
-  Float32List asFloat32List([int offsetInBytes = 0, int? length]) {
+  NativeFloat32List asFloat32List([int offsetInBytes = 0, int? length]) {
     return NativeFloat32List.view(this, offsetInBytes, length);
   }
 
-  Float64List asFloat64List([int offsetInBytes = 0, int? length]) {
+  NativeFloat64List asFloat64List([int offsetInBytes = 0, int? length]) {
     return NativeFloat64List.view(this, offsetInBytes, length);
   }
 
-  Float32x4List asFloat32x4List([int offsetInBytes = 0, int? length]) {
+  NativeFloat32x4List asFloat32x4List([int offsetInBytes = 0, int? length]) {
     length ??= (lengthInBytes - offsetInBytes) ~/ Float32x4List.bytesPerElement;
     var storage = this.asFloat32List(offsetInBytes, length * 4);
     return NativeFloat32x4List._externalStorage(storage);
   }
 
-  Float64x2List asFloat64x2List([int offsetInBytes = 0, int? length]) {
+  NativeFloat64x2List asFloat64x2List([int offsetInBytes = 0, int? length]) {
     length ??= (lengthInBytes - offsetInBytes) ~/ Float64x2List.bytesPerElement;
     var storage = this.asFloat64List(offsetInBytes, length * 2);
     return NativeFloat64x2List._externalStorage(storage);
   }
 
-  ByteData asByteData([int offsetInBytes = 0, int? length]) {
+  NativeByteData asByteData([int offsetInBytes = 0, int? length]) {
     return NativeByteData.view(this, offsetInBytes, length);
   }
 }
@@ -109,7 +112,7 @@
 final class NativeFloat32x4List extends Object
     with ListMixin<Float32x4>, FixedLengthListMixin<Float32x4>
     implements Float32x4List, TrustedGetRuntimeType {
-  final Float32List _storage;
+  final NativeFloat32List _storage;
 
   /// Creates a [Float32x4List] of the specified length (in elements),
   /// all of whose elements are initially zero.
@@ -168,7 +171,8 @@
     _storage[(index * 4) + 3] = value.w;
   }
 
-  Float32x4List asUnmodifiableView() => _UnmodifiableFloat32x4ListView(this);
+  Float32x4List asUnmodifiableView() =>
+      _UnmodifiableFloat32x4ListView(this._storage);
 
   Float32x4List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
@@ -177,19 +181,30 @@
   }
 }
 
+/// View of a [Float32x4List] that disallows modification.
+final class _UnmodifiableFloat32x4ListView extends NativeFloat32x4List
+    with UnmodifiableListMixin<Float32x4> {
+  _UnmodifiableFloat32x4ListView(super._storage) : super._externalStorage();
+
+  ByteBuffer get buffer =>
+      _UnmodifiableNativeByteBufferView(_storage._nativeBuffer);
+
+  Float32x4List asUnmodifiableView() => this;
+}
+
 /// A fixed-length list of Int32x4 numbers that is viewable as a
 /// [TypedData]. For long lists, this implementation will be considerably more
 /// space- and time-efficient than the default [List] implementation.
 final class NativeInt32x4List extends Object
     with ListMixin<Int32x4>, FixedLengthListMixin<Int32x4>
     implements Int32x4List, TrustedGetRuntimeType {
-  final Int32List _storage;
+  final NativeInt32List _storage;
 
   /// Creates a [Int32x4List] of the specified length (in elements),
   /// all of whose elements are initially zero.
   NativeInt32x4List(int length) : _storage = NativeInt32List(length * 4);
 
-  NativeInt32x4List._externalStorage(Int32List storage) : _storage = storage;
+  NativeInt32x4List._externalStorage(this._storage);
 
   NativeInt32x4List._slowFromList(List<Int32x4> list)
       : _storage = NativeInt32List(list.length * 4) {
@@ -242,7 +257,7 @@
     _storage[(index * 4) + 3] = value.w;
   }
 
-  Int32x4List asUnmodifiableView() => _UnmodifiableInt32x4ListView(this);
+  Int32x4List asUnmodifiableView() => _UnmodifiableInt32x4ListView(_storage);
 
   Int32x4List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
@@ -251,13 +266,24 @@
   }
 }
 
+/// View of a [Int32x4List] that disallows modification.
+final class _UnmodifiableInt32x4ListView extends NativeInt32x4List
+    with UnmodifiableListMixin<Int32x4> {
+  _UnmodifiableInt32x4ListView(super._storage) : super._externalStorage();
+
+  ByteBuffer get buffer =>
+      _UnmodifiableNativeByteBufferView(_storage._nativeBuffer);
+
+  Int32x4List asUnmodifiableView() => this;
+}
+
 /// A fixed-length list of Float64x2 numbers that is viewable as a
 /// [TypedData]. For long lists, this implementation will be considerably more
 /// space- and time-efficient than the default [List] implementation.
 final class NativeFloat64x2List extends Object
     with ListMixin<Float64x2>, FixedLengthListMixin<Float64x2>
     implements Float64x2List, TrustedGetRuntimeType {
-  final Float64List _storage;
+  final NativeFloat64List _storage;
 
   /// Creates a [Float64x2List] of the specified length (in elements),
   /// all of whose elements are initially zero.
@@ -310,7 +336,8 @@
     _storage[(index * 2) + 1] = value.y;
   }
 
-  Float64x2List asUnmodifiableView() => _UnmodifiableFloat64x2ListView(this);
+  Float64x2List asUnmodifiableView() =>
+      _UnmodifiableFloat64x2ListView(this._storage);
 
   Float64x2List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
@@ -319,12 +346,35 @@
   }
 }
 
+/// View of a [Float64x2List] that disallows modification.
+final class _UnmodifiableFloat64x2ListView extends NativeFloat64x2List
+    with UnmodifiableListMixin<Float64x2> {
+  _UnmodifiableFloat64x2ListView(super._storage) : super._externalStorage();
+
+  ByteBuffer get buffer =>
+      _UnmodifiableNativeByteBufferView(_storage._nativeBuffer);
+
+  Float64x2List asUnmodifiableView() => this;
+}
+
 @Native('ArrayBufferView')
 final class NativeTypedData extends JavaScriptObject implements TypedData {
   /// Returns the byte buffer associated with this object.
+  ByteBuffer get buffer {
+    if (_isUnmodifiable()) {
+      return _UnmodifiableNativeByteBufferView(_nativeBuffer);
+    } else {
+      // TODO(sra): Consider always wrapping the ArrayBuffer - this would make
+      // user-code accesses monomorphic in the ByteBuffer implementation, but
+      // might cause interop problems, e.g., with `postMessage`.
+      return _nativeBuffer;
+    }
+  }
+
   @Creates('NativeByteBuffer')
   @Returns('NativeByteBuffer')
-  ByteBuffer get buffer native;
+  @JSName('buffer')
+  NativeByteBuffer get _nativeBuffer native;
 
   /// Returns the length of this view, in bytes.
   @JSName('byteLength')
@@ -339,6 +389,10 @@
   @JSName('BYTES_PER_ELEMENT')
   int get elementSizeInBytes native;
 
+  bool _isUnmodifiable() {
+    return HArrayFlagsGet(this) & ArrayFlags.unmodifiableCheck != 0;
+  }
+
   void _invalidPosition(int position, int length, String name) {
     if (position is! int) {
       throw ArgumentError.value(position, name, 'Invalid list position');
@@ -354,6 +408,95 @@
       _invalidPosition(position, length, name);
     }
   }
+
+  @pragma('dart2js:prefer-inline')
+  void _checkMutable(String operation) {
+    HArrayFlagsCheck(
+        this, HArrayFlagsGet(this), ArrayFlags.unmodifiableCheck, operation);
+  }
+}
+
+/// A view of a ByteBuffer (ArrayBuffer) that yields only unmodifiable views.
+///
+/// The underlying ByteBuffer may have both modifiable and unmodifiable views.
+final class _UnmodifiableNativeByteBufferView implements ByteBuffer {
+  final NativeByteBuffer _data;
+
+  _UnmodifiableNativeByteBufferView(this._data);
+
+  int get lengthInBytes => _data.lengthInBytes;
+
+  NativeUint8List asUint8List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asUint8List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Int8List asInt8List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asInt8List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Uint8ClampedList asUint8ClampedList([int offsetInBytes = 0, int? length]) {
+    final result = _data.asUint8ClampedList(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Uint16List asUint16List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asUint16List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Int16List asInt16List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asInt16List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Uint32List asUint32List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asUint32List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Int32List asInt32List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asInt32List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Uint64List asUint64List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asUint64List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Int64List asInt64List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asInt64List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Int32x4List asInt32x4List([int offsetInBytes = 0, int? length]) {
+    return _data.asInt32x4List(offsetInBytes, length).asUnmodifiableView();
+  }
+
+  Float32List asFloat32List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asFloat32List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Float64List asFloat64List([int offsetInBytes = 0, int? length]) {
+    final result = _data.asFloat64List(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
+
+  Float32x4List asFloat32x4List([int offsetInBytes = 0, int? length]) {
+    return _data.asFloat32x4List(offsetInBytes, length).asUnmodifiableView();
+  }
+
+  Float64x2List asFloat64x2List([int offsetInBytes = 0, int? length]) {
+    return _data.asFloat64x2List(offsetInBytes, length).asUnmodifiableView();
+  }
+
+  ByteData asByteData([int offsetInBytes = 0, int? length]) {
+    final result = _data.asByteData(offsetInBytes, length);
+    return HArrayFlagsSet(result, ArrayFlags.unmodifiable);
+  }
 }
 
 // Validates the unnamed constructor length argument.  Checking is necessary
@@ -420,7 +563,11 @@
 
   int get elementSizeInBytes => 1;
 
-  ByteData asUnmodifiableView() => _UnmodifiableByteDataView(this);
+  ByteData asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, lengthInBytes),
+        ArrayFlags.unmodifiable);
+  }
 
   /// Returns the floating point number represented by the four bytes at
   /// the specified [byteOffset] in this object, in IEEE 754
@@ -433,6 +580,7 @@
 
   @JSName('getFloat32')
   @Returns('double')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   double _getFloat32(int byteOffset, [bool? littleEndian]) native;
 
   /// Returns the floating point number represented by the eight bytes at
@@ -446,6 +594,7 @@
 
   @JSName('getFloat64')
   @Returns('double')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   double _getFloat64(int byteOffset, [bool? littleEndian]) native;
 
   /// Returns the (possibly negative) integer represented by the two bytes at
@@ -461,6 +610,7 @@
 
   @JSName('getInt16')
   @Returns('int')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   int _getInt16(int byteOffset, [bool? littleEndian]) native;
 
   /// Returns the (possibly negative) integer represented by the four bytes at
@@ -476,6 +626,7 @@
 
   @JSName('getInt32')
   @Returns('int')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   int _getInt32(int byteOffset, [bool? littleEndian]) native;
 
   /// Returns the (possibly negative) integer represented by the eight bytes at
@@ -510,6 +661,7 @@
 
   @JSName('getUint16')
   @Returns('JSUInt31')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   int _getUint16(int byteOffset, [bool? littleEndian]) native;
 
   /// Returns the positive integer represented by the four bytes starting
@@ -524,6 +676,7 @@
 
   @JSName('getUint32')
   @Returns('JSUInt32')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   int _getUint32(int byteOffset, [bool? littleEndian]) native;
 
   /// Returns the positive integer represented by the eight bytes starting
@@ -560,10 +713,14 @@
   ///
   /// The [byteOffset] must be non-negative, and
   /// `byteOffset + 4` must be less than or equal to the length of this object.
-  void setFloat32(int byteOffset, num value, [Endian endian = Endian.big]) =>
-      _setFloat32(byteOffset, value, Endian.little == endian);
+  @pragma('dart2js:prefer-inline')
+  void setFloat32(int byteOffset, num value, [Endian endian = Endian.big]) {
+    _checkMutable('setFloat32');
+    _setFloat32(byteOffset, value, Endian.little == endian);
+  }
 
   @JSName('setFloat32')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   void _setFloat32(int byteOffset, num value, [bool? littleEndian]) native;
 
   /// Sets the eight bytes starting at the specified [byteOffset] in this
@@ -572,10 +729,14 @@
   ///
   /// The [byteOffset] must be non-negative, and
   /// `byteOffset + 8` must be less than or equal to the length of this object.
-  void setFloat64(int byteOffset, num value, [Endian endian = Endian.big]) =>
-      _setFloat64(byteOffset, value, Endian.little == endian);
+  @pragma('dart2js:prefer-inline')
+  void setFloat64(int byteOffset, num value, [Endian endian = Endian.big]) {
+    _checkMutable('setFloat64');
+    _setFloat64(byteOffset, value, Endian.little == endian);
+  }
 
   @JSName('setFloat64')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   void _setFloat64(int byteOffset, num value, [bool? littleEndian]) native;
 
   /// Sets the two bytes starting at the specified [byteOffset] in this
@@ -585,10 +746,14 @@
   ///
   /// The [byteOffset] must be non-negative, and
   /// `byteOffset + 2` must be less than or equal to the length of this object.
-  void setInt16(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _setInt16(byteOffset, value, Endian.little == endian);
+  @pragma('dart2js:prefer-inline')
+  void setInt16(int byteOffset, int value, [Endian endian = Endian.big]) {
+    _checkMutable('setInt16');
+    _setInt16(byteOffset, value, Endian.little == endian);
+  }
 
   @JSName('setInt16')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   void _setInt16(int byteOffset, int value, [bool? littleEndian]) native;
 
   /// Sets the four bytes starting at the specified [byteOffset] in this
@@ -598,10 +763,14 @@
   ///
   /// The [byteOffset] must be non-negative, and
   /// `byteOffset + 4` must be less than or equal to the length of this object.
-  void setInt32(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _setInt32(byteOffset, value, Endian.little == endian);
+  @pragma('dart2js:prefer-inline')
+  void setInt32(int byteOffset, int value, [Endian endian = Endian.big]) {
+    _checkMutable('setInt32');
+    _setInt32(byteOffset, value, Endian.little == endian);
+  }
 
   @JSName('setInt32')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   void _setInt32(int byteOffset, int value, [bool? littleEndian]) native;
 
   /// Sets the eight bytes starting at the specified [byteOffset] in this
@@ -622,7 +791,15 @@
   ///
   /// The [byteOffset] must be non-negative, and
   /// less than the length of this object.
-  void setInt8(int byteOffset, int value) native;
+  @pragma('dart2js:prefer-inline')
+  void setInt8(int byteOffset, int value) {
+    _checkMutable('setInt8');
+    _setInt8(byteOffset, value);
+  }
+
+  @JSName('setInt8')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
+  void _setInt8(int byteOffset, int value) native;
 
   /// Sets the two bytes starting at the specified [byteOffset] in this object
   /// to the unsigned binary representation of the specified [value],
@@ -631,10 +808,14 @@
   ///
   /// The [byteOffset] must be non-negative, and
   /// `byteOffset + 2` must be less than or equal to the length of this object.
-  void setUint16(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _setUint16(byteOffset, value, Endian.little == endian);
+  @pragma('dart2js:prefer-inline')
+  void setUint16(int byteOffset, int value, [Endian endian = Endian.big]) {
+    _checkMutable('setUint16');
+    _setUint16(byteOffset, value, Endian.little == endian);
+  }
 
   @JSName('setUint16')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   void _setUint16(int byteOffset, int value, [bool? littleEndian]) native;
 
   /// Sets the four bytes starting at the specified [byteOffset] in this object
@@ -644,10 +825,14 @@
   ///
   /// The [byteOffset] must be non-negative, and
   /// `byteOffset + 4` must be less than or equal to the length of this object.
-  void setUint32(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _setUint32(byteOffset, value, Endian.little == endian);
+  @pragma('dart2js:prefer-inline')
+  void setUint32(int byteOffset, int value, [Endian endian = Endian.big]) {
+    _checkMutable('setUint32');
+    _setUint32(byteOffset, value, Endian.little == endian);
+  }
 
   @JSName('setUint32')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
   void _setUint32(int byteOffset, int value, [bool? littleEndian]) native;
 
   /// Sets the eight bytes starting at the specified [byteOffset] in this object
@@ -668,10 +853,20 @@
   ///
   /// The [byteOffset] must be non-negative, and
   /// less than the length of this object.
-  void setUint8(int byteOffset, int value) native;
+  @pragma('dart2js:prefer-inline')
+  void setUint8(int byteOffset, int value) {
+    _checkMutable('setUint8');
+    _setUint8(byteOffset, value);
+  }
 
-  static NativeByteData _create1(arg) =>
-      JS('NativeByteData', 'new DataView(new ArrayBuffer(#))', arg);
+  @JSName('setUint8')
+  @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove.
+  void _setUint8(int byteOffset, int value) native;
+
+  static NativeByteData _create1(arg) => JS(
+      'returns:NativeByteData;effects:none;depends:none;new:true',
+      'new DataView(new ArrayBuffer(#))',
+      arg);
 
   static NativeByteData _create2(arg1, arg2) =>
       JS('NativeByteData', 'new DataView(#, #)', arg1, arg2);
@@ -715,12 +910,14 @@
   }
 
   void operator []=(int index, double value) {
+    _checkMutable('[]=');
     _checkValidIndex(index, this, this.length);
     JS('void', '#[#] = #', this, index, value);
   }
 
   void setRange(int start, int end, Iterable<double> iterable,
       [int skipCount = 0]) {
+    _checkMutable('setRange');
     if (iterable is NativeTypedArrayOfDouble) {
       _setRangeFast(start, end, iterable, skipCount);
       return;
@@ -736,12 +933,14 @@
   // types
 
   void operator []=(int index, int value) {
+    _checkMutable('[]=');
     _checkValidIndex(index, this, this.length);
     JS('void', '#[#] = #', this, index, value);
   }
 
   void setRange(int start, int end, Iterable<int> iterable,
       [int skipCount = 0]) {
+    _checkMutable('setRange');
     if (iterable is NativeTypedArrayOfInt) {
       _setRangeFast(start, end, iterable, skipCount);
       return;
@@ -768,9 +967,13 @@
 
   Type get runtimeType => Float32List;
 
-  Float32List asUnmodifiableView() => _UnmodifiableFloat32ListView(this);
+  Float32List asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Float32List sublist(int start, [int? end]) {
+  NativeFloat32List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source = JS('NativeFloat32List', '#.subarray(#, #)', this, start, stop);
     return _create1(source);
@@ -806,9 +1009,13 @@
 
   Type get runtimeType => Float64List;
 
-  Float64List asUnmodifiableView() => _UnmodifiableFloat64ListView(this);
+  Float64List asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Float64List sublist(int start, [int? end]) {
+  NativeFloat64List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source = JS('NativeFloat64List', '#.subarray(#, #)', this, start, stop);
     return _create1(source);
@@ -849,9 +1056,13 @@
     return JS('int', '#[#]', this, index);
   }
 
-  Int16List asUnmodifiableView() => _UnmodifiableInt16ListView(this);
+  Int16List asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Int16List sublist(int start, [int? end]) {
+  NativeInt16List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source = JS('NativeInt16List', '#.subarray(#, #)', this, start, stop);
     return _create1(source);
@@ -892,9 +1103,13 @@
     return JS('int', '#[#]', this, index);
   }
 
-  Int32List asUnmodifiableView() => _UnmodifiableInt32ListView(this);
+  Int32List asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Int32List sublist(int start, [int? end]) {
+  NativeInt32List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source = JS('NativeInt32List', '#.subarray(#, #)', this, start, stop);
     return _create1(source);
@@ -935,9 +1150,13 @@
     return JS('int', '#[#]', this, index);
   }
 
-  Int8List asUnmodifiableView() => _UnmodifiableInt8ListView(this);
+  Int8List asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Int8List sublist(int start, [int? end]) {
+  NativeInt8List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source = JS('NativeInt8List', '#.subarray(#, #)', this, start, stop);
     return _create1(source);
@@ -981,9 +1200,13 @@
     return JS('JSUInt31', '#[#]', this, index);
   }
 
-  Uint16List asUnmodifiableView() => _UnmodifiableUint16ListView(this);
+  Uint16List asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Uint16List sublist(int start, [int? end]) {
+  NativeUint16List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source = JS('NativeUint16List', '#.subarray(#, #)', this, start, stop);
     return _create1(source);
@@ -1024,9 +1247,13 @@
     return JS('JSUInt32', '#[#]', this, index);
   }
 
-  Uint32List asUnmodifiableView() => _UnmodifiableUint32ListView(this);
+  Uint32List asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Uint32List sublist(int start, [int? end]) {
+  NativeUint32List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source = JS('NativeUint32List', '#.subarray(#, #)', this, start, stop);
     return _create1(source);
@@ -1070,10 +1297,13 @@
     return JS('JSUInt31', '#[#]', this, index);
   }
 
-  Uint8ClampedList asUnmodifiableView() =>
-      _UnmodifiableUint8ClampedListView(this);
+  Uint8ClampedList asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Uint8ClampedList sublist(int start, [int? end]) {
+  NativeUint8ClampedList sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source =
         JS('NativeUint8ClampedList', '#.subarray(#, #)', this, start, stop);
@@ -1128,9 +1358,13 @@
     return JS('JSUInt31', '#[#]', this, index);
   }
 
-  Uint8List asUnmodifiableView() => _UnmodifiableUint8ListView(this);
+  Uint8List asUnmodifiableView() {
+    if (_isUnmodifiable()) return this;
+    return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length),
+        ArrayFlags.unmodifiable);
+  }
 
-  Uint8List sublist(int start, [int? end]) {
+  NativeUint8List sublist(int start, [int? end]) {
     var stop = _checkValidRange(start, end, this.length);
     var source = JS('NativeUint8List', '#.subarray(#, #)', this, start, stop);
     return _create1(source);
@@ -1888,335 +2122,3 @@
   if (end == null) return length;
   return end;
 }
-
-/// A read-only view of a [ByteBuffer].
-final class _UnmodifiableByteBufferView implements ByteBuffer {
-  final ByteBuffer _data;
-
-  _UnmodifiableByteBufferView(ByteBuffer data) : _data = data;
-
-  int get lengthInBytes => _data.lengthInBytes;
-
-  Uint8List asUint8List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableUint8ListView(_data.asUint8List(offsetInBytes, length));
-
-  Int8List asInt8List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableInt8ListView(_data.asInt8List(offsetInBytes, length));
-
-  Uint8ClampedList asUint8ClampedList([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableUint8ClampedListView(
-          _data.asUint8ClampedList(offsetInBytes, length));
-
-  Uint16List asUint16List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableUint16ListView(_data.asUint16List(offsetInBytes, length));
-
-  Int16List asInt16List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableInt16ListView(_data.asInt16List(offsetInBytes, length));
-
-  Uint32List asUint32List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableUint32ListView(_data.asUint32List(offsetInBytes, length));
-
-  Int32List asInt32List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableInt32ListView(_data.asInt32List(offsetInBytes, length));
-
-  Uint64List asUint64List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableUint64ListView(_data.asUint64List(offsetInBytes, length));
-
-  Int64List asInt64List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableInt64ListView(_data.asInt64List(offsetInBytes, length));
-
-  Int32x4List asInt32x4List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableInt32x4ListView(_data.asInt32x4List(offsetInBytes, length));
-
-  Float32List asFloat32List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableFloat32ListView(_data.asFloat32List(offsetInBytes, length));
-
-  Float64List asFloat64List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableFloat64ListView(_data.asFloat64List(offsetInBytes, length));
-
-  Float32x4List asFloat32x4List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableFloat32x4ListView(
-          _data.asFloat32x4List(offsetInBytes, length));
-
-  Float64x2List asFloat64x2List([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableFloat64x2ListView(
-          _data.asFloat64x2List(offsetInBytes, length));
-
-  ByteData asByteData([int offsetInBytes = 0, int? length]) =>
-      _UnmodifiableByteDataView(_data.asByteData(offsetInBytes, length));
-}
-
-/// A read-only view of a [ByteData].
-final class _UnmodifiableByteDataView implements ByteData {
-  final ByteData _data;
-
-  _UnmodifiableByteDataView(ByteData data) : _data = data;
-
-  ByteData asUnmodifiableView() => this;
-
-  int getInt8(int byteOffset) => _data.getInt8(byteOffset);
-
-  void setInt8(int byteOffset, int value) => _unsupported();
-
-  int getUint8(int byteOffset) => _data.getUint8(byteOffset);
-
-  void setUint8(int byteOffset, int value) => _unsupported();
-
-  int getInt16(int byteOffset, [Endian endian = Endian.big]) =>
-      _data.getInt16(byteOffset, endian);
-
-  void setInt16(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _unsupported();
-
-  int getUint16(int byteOffset, [Endian endian = Endian.big]) =>
-      _data.getUint16(byteOffset, endian);
-
-  void setUint16(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _unsupported();
-
-  int getInt32(int byteOffset, [Endian endian = Endian.big]) =>
-      _data.getInt32(byteOffset, endian);
-
-  void setInt32(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _unsupported();
-
-  int getUint32(int byteOffset, [Endian endian = Endian.big]) =>
-      _data.getUint32(byteOffset, endian);
-
-  void setUint32(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _unsupported();
-
-  int getInt64(int byteOffset, [Endian endian = Endian.big]) =>
-      _data.getInt64(byteOffset, endian);
-
-  void setInt64(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _unsupported();
-
-  int getUint64(int byteOffset, [Endian endian = Endian.big]) =>
-      _data.getUint64(byteOffset, endian);
-
-  void setUint64(int byteOffset, int value, [Endian endian = Endian.big]) =>
-      _unsupported();
-
-  double getFloat32(int byteOffset, [Endian endian = Endian.big]) =>
-      _data.getFloat32(byteOffset, endian);
-
-  void setFloat32(int byteOffset, double value, [Endian endian = Endian.big]) =>
-      _unsupported();
-
-  double getFloat64(int byteOffset, [Endian endian = Endian.big]) =>
-      _data.getFloat64(byteOffset, endian);
-
-  void setFloat64(int byteOffset, double value, [Endian endian = Endian.big]) =>
-      _unsupported();
-
-  int get elementSizeInBytes => _data.elementSizeInBytes;
-
-  int get offsetInBytes => _data.offsetInBytes;
-
-  int get lengthInBytes => _data.lengthInBytes;
-
-  ByteBuffer get buffer => _UnmodifiableByteBufferView(_data.buffer);
-
-  void _unsupported() {
-    throw UnsupportedError("An UnmodifiableByteDataView may not be modified");
-  }
-}
-
-mixin _UnmodifiableListMixin<N, L extends List<N>, TD extends TypedData> {
-  L get _list;
-  TD get _data => (_list as TD);
-
-  int get length => _list.length;
-
-  N operator [](int index) => _list[index];
-
-  int get elementSizeInBytes => _data.elementSizeInBytes;
-
-  int get offsetInBytes => _data.offsetInBytes;
-
-  int get lengthInBytes => _data.lengthInBytes;
-
-  ByteBuffer get buffer => _UnmodifiableByteBufferView(_data.buffer);
-
-  L _createList(int length);
-
-  L sublist(int start, [int? end]) {
-    // NNBD: Spurious error at `end`, `checkValidRange` is legacy.
-    int endIndex = RangeError.checkValidRange(start, end!, length);
-    int sublistLength = endIndex - start;
-    L result = _createList(sublistLength);
-    result.setRange(0, sublistLength, _list, start);
-    return result;
-  }
-}
-
-/// View of a [Uint8List] that disallows modification.
-final class _UnmodifiableUint8ListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Uint8List, Uint8List>
-    implements Uint8List {
-  final Uint8List _list;
-  _UnmodifiableUint8ListView(Uint8List list) : _list = list;
-
-  Uint8List asUnmodifiableView() => this;
-
-  Uint8List _createList(int length) => Uint8List(length);
-}
-
-/// View of a [Int8List] that disallows modification.
-final class _UnmodifiableInt8ListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Int8List, Int8List>
-    implements Int8List {
-  final Int8List _list;
-  _UnmodifiableInt8ListView(Int8List list) : _list = list;
-
-  Int8List asUnmodifiableView() => this;
-
-  Int8List _createList(int length) => Int8List(length);
-}
-
-/// View of a [Uint8ClampedList] that disallows modification.
-final class _UnmodifiableUint8ClampedListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Uint8ClampedList, Uint8ClampedList>
-    implements Uint8ClampedList {
-  final Uint8ClampedList _list;
-  _UnmodifiableUint8ClampedListView(Uint8ClampedList list) : _list = list;
-
-  Uint8ClampedList asUnmodifiableView() => this;
-
-  Uint8ClampedList _createList(int length) => Uint8ClampedList(length);
-}
-
-/// View of a [Uint16List] that disallows modification.
-final class _UnmodifiableUint16ListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Uint16List, Uint16List>
-    implements Uint16List {
-  final Uint16List _list;
-  _UnmodifiableUint16ListView(Uint16List list) : _list = list;
-
-  Uint16List asUnmodifiableView() => this;
-
-  Uint16List _createList(int length) => Uint16List(length);
-}
-
-/// View of a [Int16List] that disallows modification.
-final class _UnmodifiableInt16ListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Int16List, Int16List>
-    implements Int16List {
-  final Int16List _list;
-  _UnmodifiableInt16ListView(Int16List list) : _list = list;
-
-  Int16List asUnmodifiableView() => this;
-
-  Int16List _createList(int length) => Int16List(length);
-}
-
-/// View of a [Uint32List] that disallows modification.
-final class _UnmodifiableUint32ListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Uint32List, Uint32List>
-    implements Uint32List {
-  final Uint32List _list;
-  _UnmodifiableUint32ListView(Uint32List list) : _list = list;
-
-  Uint32List asUnmodifiableView() => this;
-
-  Uint32List _createList(int length) => Uint32List(length);
-}
-
-/// View of a [Int32List] that disallows modification.
-final class _UnmodifiableInt32ListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Int32List, Int32List>
-    implements Int32List {
-  final Int32List _list;
-  _UnmodifiableInt32ListView(Int32List list) : _list = list;
-
-  Int32List asUnmodifiableView() => this;
-
-  Int32List _createList(int length) => Int32List(length);
-}
-
-/// View of a [Uint64List] that disallows modification.
-final class _UnmodifiableUint64ListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Uint64List, Uint64List>
-    implements Uint64List {
-  final Uint64List _list;
-  _UnmodifiableUint64ListView(Uint64List list) : _list = list;
-
-  Uint64List asUnmodifiableView() => this;
-
-  Uint64List _createList(int length) => Uint64List(length);
-}
-
-/// View of a [Int64List] that disallows modification.
-final class _UnmodifiableInt64ListView extends UnmodifiableListBase<int>
-    with _UnmodifiableListMixin<int, Int64List, Int64List>
-    implements Int64List {
-  final Int64List _list;
-  _UnmodifiableInt64ListView(Int64List list) : _list = list;
-
-  Int64List asUnmodifiableView() => this;
-
-  Int64List _createList(int length) => Int64List(length);
-}
-
-/// View of a [Int32x4List] that disallows modification.
-final class _UnmodifiableInt32x4ListView extends UnmodifiableListBase<Int32x4>
-    with _UnmodifiableListMixin<Int32x4, Int32x4List, Int32x4List>
-    implements Int32x4List {
-  final Int32x4List _list;
-  _UnmodifiableInt32x4ListView(Int32x4List list) : _list = list;
-
-  Int32x4List asUnmodifiableView() => this;
-
-  Int32x4List _createList(int length) => Int32x4List(length);
-}
-
-/// View of a [Float32x4List] that disallows modification.
-final class _UnmodifiableFloat32x4ListView
-    extends UnmodifiableListBase<Float32x4>
-    with _UnmodifiableListMixin<Float32x4, Float32x4List, Float32x4List>
-    implements Float32x4List {
-  final Float32x4List _list;
-  _UnmodifiableFloat32x4ListView(Float32x4List list) : _list = list;
-
-  Float32x4List asUnmodifiableView() => this;
-
-  Float32x4List _createList(int length) => Float32x4List(length);
-}
-
-/// View of a [Float64x2List] that disallows modification.
-final class _UnmodifiableFloat64x2ListView
-    extends UnmodifiableListBase<Float64x2>
-    with _UnmodifiableListMixin<Float64x2, Float64x2List, Float64x2List>
-    implements Float64x2List {
-  final Float64x2List _list;
-  _UnmodifiableFloat64x2ListView(Float64x2List list) : _list = list;
-
-  Float64x2List asUnmodifiableView() => this;
-
-  Float64x2List _createList(int length) => Float64x2List(length);
-}
-
-/// View of a [Float32List] that disallows modification.
-final class _UnmodifiableFloat32ListView extends UnmodifiableListBase<double>
-    with _UnmodifiableListMixin<double, Float32List, Float32List>
-    implements Float32List {
-  final Float32List _list;
-  _UnmodifiableFloat32ListView(Float32List list) : _list = list;
-
-  Float32List asUnmodifiableView() => this;
-
-  Float32List _createList(int length) => Float32List(length);
-}
-
-/// View of a [Float64List] that disallows modification.
-final class _UnmodifiableFloat64ListView extends UnmodifiableListBase<double>
-    with _UnmodifiableListMixin<double, Float64List, Float64List>
-    implements Float64List {
-  final Float64List _list;
-  _UnmodifiableFloat64ListView(Float64List list) : _list = list;
-
-  Float64List asUnmodifiableView() => this;
-
-  Float64List _createList(int length) => Float64List(length);
-}
diff --git a/sdk/lib/_internal/js_runtime/lib/synced/array_flags.dart b/sdk/lib/_internal/js_runtime/lib/synced/array_flags.dart
new file mode 100644
index 0000000..d8ca41e
--- /dev/null
+++ b/sdk/lib/_internal/js_runtime/lib/synced/array_flags.dart
@@ -0,0 +1,63 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Values for the 'array flags' property that is attached to native JSArray and
+/// typed data objects to encode restrictions.
+///
+/// We encode the restrictions as two bits - one for potentially growable
+/// objects that are restricted to be fixed length, and a second for objects
+/// with potentially modifiable contents that are restricted to have
+/// unmodifiable contents. We only use three combinations (00 = growable, 01 =
+/// fixed-length, 11 = unmodifiable), since the core List class does not have a
+/// variant that is an 'append-only' list (would be 10).
+///
+/// We use the same scheme for typed data so that the same check works for
+/// modifiability checks for `a[i] = v` when `a` is a JSarray or Uint8Array.
+///
+/// `const` JSArray values are tagged with an additional bit for future use in
+/// avoiding copying. We could also use the `const` bit for unmodifiable typed
+/// data for which there in no modifable view of the underlying buffer.
+///
+/// Given the absense of append-only lists, we could have used a more
+/// numerically compact scheme (0 = growable, 1 = fixed-length, 2 =
+/// unmodifiable, 3 = const). This compact scheme requires comparison
+///
+///     if (x.flags > 0)
+///
+/// rather than mask-testing
+///
+///     if (x.flags & 1)
+///
+/// The unrestricted flags (0) are usually encoded as a missing property, i.e.,
+/// `undefined`, which is converted to NaN for '>', but converted to the more
+/// comfortable '0' for '&'. We hope that there is less to go potentially go
+/// wrong with JavaScript engine slow paths with the bitmask.
+class ArrayFlags {
+  /// Value of array flags that marks a JSArray as being fixed-length. This is
+  /// not used on typed data since that is always fixed-length.
+  static const int fixedLength = fixedLengthCheck;
+
+  /// Value of array flags that marks a JSArray or typed-data object as
+  /// unmodifiable. Includes the 'fixed-length' bit, since all unmodifiable
+  /// lists are also fixed length.
+  static const int unmodifiable = fixedLengthCheck | unmodifiableCheck;
+
+  /// Value of array flags that marks a JSArray as a constant (transitively
+  /// unmodifiable).
+  static const int constant =
+      fixedLengthCheck | unmodifiableCheck | constantCheck;
+
+  /// Default value of array flags when there is no flags property. This value
+  /// is not stored on the flags property.
+  static const int none = 0;
+
+  /// Bit to check for fixed-length JSArray.
+  static const int fixedLengthCheck = 1;
+
+  /// Bit to check for unmodifiable JSArray and typed data.
+  static const int unmodifiableCheck = 2;
+
+  /// Bit to check for constant JSArray.
+  static const int constantCheck = 4;
+}
diff --git a/sdk/lib/libraries.json b/sdk/lib/libraries.json
index 280a598..5afeeb7 100644
--- a/sdk/lib/libraries.json
+++ b/sdk/lib/libraries.json
@@ -394,6 +394,9 @@
   },
   "_dart2js_common": {
     "libraries": {
+      "_array_flags": {
+        "uri": "_internal/js_runtime/lib/synced/array_flags.dart"
+      },
       "async": {
         "uri": "async/async.dart",
         "patches": "_internal/js_runtime/lib/async_patch.dart"
diff --git a/sdk/lib/libraries.yaml b/sdk/lib/libraries.yaml
index e06c5cd..3614fc8 100644
--- a/sdk/lib/libraries.yaml
+++ b/sdk/lib/libraries.yaml
@@ -318,6 +318,9 @@
 
 _dart2js_common:
   libraries:
+    _array_flags:
+      uri: "_internal/js_runtime/lib/synced/array_flags.dart"
+
     async:
       uri: "async/async.dart"
       patches: "_internal/js_runtime/lib/async_patch.dart"
diff --git a/tests/web/consistent_unmodifiable_typed_data_error_test.dart b/tests/web/consistent_unmodifiable_typed_data_error_test.dart
new file mode 100644
index 0000000..5ac81c2
--- /dev/null
+++ b/tests/web/consistent_unmodifiable_typed_data_error_test.dart
@@ -0,0 +1,217 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:typed_data';
+import 'package:expect/expect.dart';
+
+// Test that unmodifiable typed data operations on optimized and slow paths
+// produce the same error.
+
+@pragma('dart2js:never-inline')
+@pragma('dart2js:assumeDynamic')
+confuse(x) => x;
+
+void check2(String name, String name1, f1(), String name2, f2()) {
+  Error? trap(part, f) {
+    try {
+      f();
+    } on Error catch (e) {
+      return e;
+    }
+    Expect.fail('should throw: $name.$part');
+  }
+
+  var e1 = trap(name1, f1);
+  var e2 = trap(name2, f2);
+  var s1 = '$e1';
+  var s2 = '$e2';
+  Expect.equals(s1, s2, '\n  $name.$name1: "$s1"\n  $name.$name2: "$s2"\n');
+}
+
+void check(String name, f1(), f2(), [f3()?, f4()?]) {
+  check2(name, 'f1', f1, 'f2', f2);
+  if (f3 != null) check2(name, 'f1', f1, 'f3', f3);
+  if (f4 != null) check2(name, 'f1', f1, 'f4', f4);
+}
+
+void main() {
+  ByteData a = ByteData(100);
+  ByteData b = a.asUnmodifiableView();
+  ByteData c = confuse(true) ? b : a;
+
+  dynamic d = confuse(true) ? b : const [1];
+
+  void setInt8Test() {
+    void f1() {
+      d.setInt8(0, 1); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setInt8(0, 1); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setInt8(0, 1); // potentially unmodifiable receiver
+    }
+
+    check('setInt8', f1, f2, f3);
+  }
+
+  void setInt16Test() {
+    void f1() {
+      d.setInt16(0, 1); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setInt16(0, 1); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setInt16(0, 1); // potentially unmodifiable receiver
+    }
+
+    check('setInt16', f1, f2, f3);
+  }
+
+  void setInt32Test() {
+    void f1() {
+      d.setInt32(0, 1); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setInt32(0, 1); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setInt32(0, 1); // potentially unmodifiable receiver
+    }
+
+    check('setInt32', f1, f2, f3);
+  }
+
+  void setInt64Test() {
+    void f1() {
+      d.setInt64(0, 1); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setInt64(0, 1); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setInt64(0, 1); // potentially unmodifiable receiver
+    }
+
+    check('setInt64', f1, f2, f3);
+  }
+
+  void setUint8Test() {
+    void f1() {
+      d.setUint8(0, 1); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setUint8(0, 1); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setUint8(0, 1); // potentially unmodifiable receiver
+    }
+
+    check('setUint8', f1, f2, f3);
+  }
+
+  void setUint16Test() {
+    void f1() {
+      d.setUint16(0, 1); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setUint16(0, 1); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setUint16(0, 1); // potentially unmodifiable receiver
+    }
+
+    check('setUint16', f1, f2, f3);
+  }
+
+  void setUint32Test() {
+    void f1() {
+      d.setUint32(0, 1); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setUint32(0, 1); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setUint32(0, 1); // potentially unmodifiable receiver
+    }
+
+    check('setUint32', f1, f2, f3);
+  }
+
+  void setUint64Test() {
+    void f1() {
+      d.setUint64(0, 1); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setUint64(0, 1); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setUint64(0, 1); // potentially unmodifiable receiver
+    }
+
+    check('setUint64', f1, f2, f3);
+  }
+
+  void setFloat32Test() {
+    void f1() {
+      d.setFloat32(0, 1.23); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setFloat32(0, 1.23); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setFloat32(0, 1.23); // potentially unmodifiable receiver
+    }
+
+    check('setFloat32', f1, f2, f3);
+  }
+
+  void setFloat64Test() {
+    void f1() {
+      d.setFloat64(0, 1.23); // dynamic receiver.
+    }
+
+    void f2() {
+      b.setFloat64(0, 1.23); // unmodifiable receiver
+    }
+
+    void f3() {
+      c.setFloat64(0, 1.23); // potentially unmodifiable receiver
+    }
+
+    check('setFloat64', f1, f2, f3);
+  }
+
+  setInt8Test();
+  setInt16Test();
+  setInt32Test();
+  setInt64Test();
+
+  setUint8Test();
+  setUint16Test();
+  setUint32Test();
+  setUint64Test();
+
+  setFloat32Test();
+  setFloat64Test();
+}
diff --git a/tests/web/consistent_unmodifiable_typed_list_error_test.dart b/tests/web/consistent_unmodifiable_typed_list_error_test.dart
new file mode 100644
index 0000000..e330d4b
--- /dev/null
+++ b/tests/web/consistent_unmodifiable_typed_list_error_test.dart
@@ -0,0 +1,154 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:typed_data';
+import 'package:expect/expect.dart';
+
+// Test that unmodifiable typed list assignments on optimized and slow paths
+// produce the same error.
+
+@pragma('dart2js:never-inline')
+@pragma('dart2js:assumeDynamic')
+confuse(x) => x;
+
+void check2(String name, String name1, f1(), String name2, f2()) {
+  Error? trap(part, f) {
+    try {
+      f();
+    } on Error catch (e) {
+      return e;
+    }
+    Expect.fail('should throw: $name.$part');
+  }
+
+  var e1 = trap(name1, f1);
+  var e2 = trap(name2, f2);
+  var s1 = '$e1';
+  var s2 = '$e2';
+  Expect.equals(s1, s2, '\n  $name.$name1: "$s1"\n  $name.$name2: "$s2"\n');
+}
+
+void check(String name, f1(), f2(), [f3()?, f4()?]) {
+  check2(name, 'f1', f1, 'f2', f2);
+  if (f3 != null) check2(name, 'f1', f1, 'f3', f3);
+  if (f4 != null) check2(name, 'f1', f1, 'f4', f4);
+}
+
+void testUint8List() {
+  Uint8List a = Uint8List(100);
+  Uint8List b = a.asUnmodifiableView();
+  Uint8List c = confuse(true) ? b : a;
+
+  dynamic d = confuse(true) ? b : const [1];
+
+  void f1() {
+    d[0] = 0; // dynamic receiver.
+  }
+
+  void f2() {
+    b[0] = 1; // unmodifiable receiver
+  }
+
+  void f3() {
+    c[0] = 1; // potentially unmodifiable receiver
+  }
+
+  check('Uint8List', f1, f2, f3);
+}
+
+void testInt16List() {
+  Int16List a = Int16List(100);
+  Int16List b = a.asUnmodifiableView();
+  Int16List c = confuse(true) ? b : a;
+
+  dynamic d = confuse(true) ? b : const [1];
+
+  void f1() {
+    d[0] = 0; // dynamic receiver.
+  }
+
+  void f2() {
+    b[0] = 1; // unmodifiable receiver
+  }
+
+  void f3() {
+    c[0] = 1; // potentially unmodifiable receiver
+  }
+
+  check('Int16List', f1, f2, f3);
+}
+
+void testFloat32List() {
+  Float32List a = Float32List(100);
+  Float32List b = a.asUnmodifiableView();
+  Float32List c = confuse(true) ? b : a;
+
+  dynamic d = confuse(true) ? b : const [1];
+
+  void f1() {
+    d[0] = 0; // dynamic receiver.
+  }
+
+  void f2() {
+    b[0] = 1; // unmodifiable receiver
+  }
+
+  void f3() {
+    c[0] = 1; // potentially unmodifiable receiver
+  }
+
+  check('Float32List', f1, f2, f3);
+}
+
+void testFloat64List() {
+  Float64List a = Float64List(100);
+  Float64List b = a.asUnmodifiableView();
+  Float64List c = confuse(true) ? b : a;
+
+  dynamic d = confuse(true) ? b : const [1];
+
+  void f1() {
+    d[0] = 0; // dynamic receiver.
+  }
+
+  void f2() {
+    b[0] = 1; // unmodifiable receiver
+  }
+
+  void f3() {
+    c[0] = 1; // potentially unmodifiable receiver
+  }
+
+  check('Float64List', f1, f2, f3);
+}
+
+void testFloat64x2List() {
+  Float64x2List a = Float64x2List(100);
+  Float64x2List b = a.asUnmodifiableView();
+  Float64x2List c = confuse(true) ? b : a;
+
+  dynamic d = confuse(true) ? b : const [1];
+
+  void f1() {
+    d[0] = a.last; // dynamic receiver.
+  }
+
+  void f2() {
+    b[0] = a.last; // unmodifiable receiver
+  }
+
+  void f3() {
+    c[0] = a.last; // potentially unmodifiable receiver
+  }
+
+  check('Float64List', f1, f2, f3);
+}
+
+main() {
+  testUint8List();
+  testInt16List();
+  testFloat32List();
+  testFloat64List();
+  testFloat64x2List();
+}