diff --git a/docs/src/guide/plugin-legacy.md b/docs/src/guide/plugin-legacy.md
index f1fdba5c..f80f2c80 100644
--- a/docs/src/guide/plugin-legacy.md
+++ b/docs/src/guide/plugin-legacy.md
@@ -1,8 +1,6 @@
 [vite_plugin_legacy]: https://github.com/ElMassimo/vite_ruby/tree/main/vite_plugin_legacy
 [plugin-legacy]: https://github.com/vitejs/vite/tree/main/packages/plugin-legacy
 [vite_legacy_javascript_tag]: https://github.com/ElMassimo/vite_ruby/blob/main/vite_plugin_legacy/lib/vite_plugin_legacy/tag_helpers.rb
-[vite_legacy_typescript_tag]: https://github.com/ElMassimo/vite_ruby/blob/main/vite_plugin_legacy/lib/vite_plugin_legacy/tag_helpers.rb
-[vite_legacy_polyfill_tag]: https://github.com/ElMassimo/vite_ruby/blob/main/vite_plugin_legacy/lib/vite_plugin_legacy/tag_helpers.rb
 
 # Plugin Legacy
 
@@ -27,11 +25,8 @@ bundle install
 
 In order to include the polyfills and script tags you can using the following helpers:
 
-- <kbd>[vite_legacy_javascript_tag]</kbd>: Render a `<script>` tag referencing a JavaScript file.
-- <kbd>[vite_legacy_typescript_tag]</kbd>: Render a `<script>` tag referencing a TypeScript file.
-- <kbd>[vite_legacy_polyfill_tag]</kbd>: Renders the polyfills necessary to enable code-splitting in legacy browsers.
+- <kbd>[vite_legacy_javascript_tag]</kbd>: Render a `<script>` tag referencing a JavaScript or TypeScript entrypoints.
 
-The polyfill is included by default when using <kbd>[vite_legacy_javascript_tag]</kbd>
 
 ```erb
 <head>
@@ -40,11 +35,11 @@ The polyfill is included by default when using <kbd>[vite_legacy_javascript_tag]
   <%= csp_meta_tag %>
   <%= vite_client_tag %>
 
+  <%= vite_legacy_javascript_tag 'application' => :javascript %>
   <%= vite_javascript_tag 'application' %>
 </head>
 <body>
   <%= yield %>
-  <%= vite_legacy_javascript_tag 'application' %>
 </body>
 ```
 
diff --git a/examples/rails/app/views/layouts/application.html.erb b/examples/rails/app/views/layouts/application.html.erb
index a8d49a3e..0f123ded 100644
--- a/examples/rails/app/views/layouts/application.html.erb
+++ b/examples/rails/app/views/layouts/application.html.erb
@@ -9,12 +9,12 @@
 
     <link rel="icon" type="image/png" href="<%= vite_asset_path('images/logo.png') %>">
 
+    <%= vite_legacy_javascript_tag 'application' => :typescript %>
     <%= vite_stylesheet_tag 'styles.scss' %>
     <%= vite_typescript_tag 'application', 'data-turbo-track': 'reload', media: 'all' %>
   </head>
 
   <body>
     <%= yield %>
-    <%= vite_legacy_typescript_tag 'application' %>
   </body>
 </html>
diff --git a/test/helper_test.rb b/test/helper_test.rb
index d3f1b77c..148cb8c9 100644
--- a/test/helper_test.rb
+++ b/test/helper_test.rb
@@ -50,9 +50,9 @@ class LegacyHelperTest < HelperTestCase
   })
 
   def test_plugin_legacy
-    assert_includes vite_legacy_javascript_tag('/app/assets/external'), '/vite-production/assets/external.a35ee0db-legacy.js'
-    assert_includes vite_legacy_typescript_tag('main.ts'), '/vite-production/assets/main.20bbd3a5-legacy.js'
-    assert_includes vite_legacy_polyfill_tag, '/vite-production/assets/polyfills-legacy.07477394.js'
+    assert_includes vite_legacy_javascript_tag('/app/assets/external' => :javascript), '/vite-production/assets/external.a35ee0db-legacy.js'
+    assert_includes vite_legacy_javascript_tag('main.ts' => :typescript), '/vite-production/assets/main.20bbd3a5-legacy.js'
+    assert_includes vite_legacy_javascript_tag('main.ts' => :typescript), '/vite-production/assets/polyfills-legacy.07477394.js'
   end
 end
 
diff --git a/vite_plugin_legacy/lib/vite_plugin_legacy/tag_helpers.rb b/vite_plugin_legacy/lib/vite_plugin_legacy/tag_helpers.rb
index eef92c4f..8e802f8a 100644
--- a/vite_plugin_legacy/lib/vite_plugin_legacy/tag_helpers.rb
+++ b/vite_plugin_legacy/lib/vite_plugin_legacy/tag_helpers.rb
@@ -2,29 +2,54 @@
 
 # Public: Allows to render HTML tags for scripts and styles processed by Vite.
 module VitePluginLegacy::TagHelpers
-  # Public: Renders a <script> tag for the specified Vite entrypoints when using
-  # @vitejs/plugin-legacy, which injects polyfills.
-  def vite_legacy_javascript_tag(name, asset_type: :javascript)
-    return if ViteRuby.instance.dev_server_running?
+  VITE_SAFARI_NOMODULE_FIX = <<-JS.html_safe.freeze
+  !function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;console.log('preventing load',e.target);e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();
+  JS
 
-    legacy_name = name.sub(/(\..+)|$/, '-legacy\1')
-    import_tag = content_tag(:script, nomodule: true) {
-      "System.import('#{ vite_asset_path(legacy_name, type: asset_type) }')".html_safe
-    }
+  # Renders code to load vite entrypoints for legacy browsers:
+  # * Safari NOMODULE fix for Safari 10, which supports modules but not `nomodule`
+  # * vite-legacy-polyfill (System.import polyfill) for browsers that do not support modules @vitejs/plugin-legacy
+  # * Dynamic import code for browsers that support modules, but not dynamic imports
+  # This helper must be called before any other Vite import tags.
+  # Accepts a hash with entrypoint names as keys and asset types (:javascript or :typescript) as values.
+  def vite_legacy_javascript_tag(entrypoints)
+    return if ViteRuby.instance.dev_server_running?
 
-    safe_join [vite_legacy_polyfill_tag, import_tag]
+    tags = []
+    safari_nomodule_fix = content_tag(:script, nil, nomodule: true) { VITE_SAFARI_NOMODULE_FIX }
+    tags.push(safari_nomodule_fix)
+    # for browsers which do not support modules at all
+    legacy_polyfill = content_tag(:script, nil, nomodule: true, id: 'vite-legacy-polyfill', src: vite_asset_path('legacy-polyfills', type: :virtual))
+    tags.push(legacy_polyfill)
+    # for browsers which support modules, but don't support dynamic import
+    legacy_fallback_tag = content_tag(:script, nil, type: 'module') do
+      vite_dynamic_fallback_inline_code(entrypoints)
+    end
+    entrypoints.each do |name, asset_type|
+      import_tag = content_tag(:script, nomodule: true) do
+        vite_legacy_import_body(name, asset_type: asset_type)
+      end
+      tags.push(import_tag)
+    end
+    tags.push(legacy_fallback_tag)
+    safe_join(tags, "\n")
   end
 
-  # Public: Same as `vite_legacy_javascript_tag`, but for TypeScript entries.
-  def vite_legacy_typescript_tag(name)
-    vite_legacy_javascript_tag(name, asset_type: :typescript)
+  def vite_dynamic_fallback_inline_code(entrypoints)
+    load_body = entrypoints.map do |name, asset_type|
+      vite_legacy_import_body(name, asset_type: asset_type)
+    end
+    load_body = safe_join(load_body, "\n")
+    # rubocop:disable Layout/LineLength
+    %{!function(){try{new Function("m","return import(m)")}catch(o){console.warn("vite: loading legacy build because dynamic import is unsupported, syntax error above should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){#{ load_body }},document.body.appendChild(n)}}();}.html_safe
+    # rubocop:enable Layout/LineLength
   end
 
-  # Internal: Renders the vite-legacy-polyfill to enable code splitting in
-  # browsers that do not support modules.
-  def vite_legacy_polyfill_tag
-    return if ViteRuby.instance.dev_server_running?
+  def vite_legacy_import_body(name, asset_type: :javascript)
+    "System.import('#{ vite_asset_path(vite_legacy_name(name), type: asset_type) }')".html_safe
+  end
 
-    content_tag(:script, nil, nomodule: true, src: vite_asset_path('legacy-polyfills', type: :virtual))
+  def vite_legacy_name(name)
+    name.sub(/(\..+)|$/, '-legacy\1')
   end
 end
diff --git a/vite_plugin_legacy/lib/vite_plugin_legacy/version.rb b/vite_plugin_legacy/lib/vite_plugin_legacy/version.rb
index ed2afee6..17dbac2e 100644
--- a/vite_plugin_legacy/lib/vite_plugin_legacy/version.rb
+++ b/vite_plugin_legacy/lib/vite_plugin_legacy/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 module VitePluginLegacy
-  VERSION = '3.0.2'
+  VERSION = '4.0.0'
 end