diff --git a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
index eebc2f21dfe4e..e5d0bb87edf66 100644
--- a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
+++ b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
@@ -632,6 +632,12 @@ impl CStore {
             .get_attr_flags(def_id.index)
             .contains(AttrFlags::MAY_HAVE_DOC_LINKS)
     }
+
+    pub fn is_doc_hidden_untracked(&self, def_id: DefId) -> bool {
+        self.get_crate_data(def_id.krate)
+            .get_attr_flags(def_id.index)
+            .contains(AttrFlags::IS_DOC_HIDDEN)
+    }
 }
 
 impl CrateStore for CStore {
diff --git a/src/librustdoc/clean/utils.rs b/src/librustdoc/clean/utils.rs
index a12f764fa8e3b..ca3a70c7236fe 100644
--- a/src/librustdoc/clean/utils.rs
+++ b/src/librustdoc/clean/utils.rs
@@ -29,11 +29,6 @@ mod tests;
 pub(crate) fn krate(cx: &mut DocContext<'_>) -> Crate {
     let module = crate::visit_ast::RustdocVisitor::new(cx).visit();
 
-    for &cnum in cx.tcx.crates(()) {
-        // Analyze doc-reachability for extern items
-        crate::visit_lib::lib_embargo_visit_item(cx, cnum.as_def_id());
-    }
-
     // Clean the crate, translating the entire librustc_ast AST to one that is
     // understood by rustdoc.
     let mut module = clean_doc_module(&module, cx);
diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs
index 10b606f425ea4..0ce43f7db8e8b 100644
--- a/src/librustdoc/core.rs
+++ b/src/librustdoc/core.rs
@@ -41,6 +41,7 @@ pub(crate) struct ResolverCaches {
     pub(crate) traits_in_scope: DefIdMap<Vec<TraitCandidate>>,
     pub(crate) all_trait_impls: Option<Vec<DefId>>,
     pub(crate) all_macro_rules: FxHashMap<Symbol, Res<NodeId>>,
+    pub(crate) extern_doc_reachable: DefIdSet,
 }
 
 pub(crate) struct DocContext<'tcx> {
@@ -363,6 +364,10 @@ pub(crate) fn run_global_ctxt(
         show_coverage,
     };
 
+    ctxt.cache
+        .effective_visibilities
+        .init(mem::take(&mut ctxt.resolver_caches.extern_doc_reachable));
+
     // Small hack to force the Sized trait to be present.
     //
     // Note that in case of `#![no_core]`, the trait is not available.
diff --git a/src/librustdoc/passes/collect_intra_doc_links/early.rs b/src/librustdoc/passes/collect_intra_doc_links/early.rs
index 42677bd849748..f690c49005d9c 100644
--- a/src/librustdoc/passes/collect_intra_doc_links/early.rs
+++ b/src/librustdoc/passes/collect_intra_doc_links/early.rs
@@ -2,6 +2,7 @@ use crate::clean::Attributes;
 use crate::core::ResolverCaches;
 use crate::passes::collect_intra_doc_links::preprocessed_markdown_links;
 use crate::passes::collect_intra_doc_links::{Disambiguator, PreprocessedMarkdownLink};
+use crate::visit_lib::early_lib_embargo_visit_item;
 
 use rustc_ast::visit::{self, AssocCtxt, Visitor};
 use rustc_ast::{self as ast, ItemKind};
@@ -34,6 +35,8 @@ pub(crate) fn early_resolve_intra_doc_links(
         traits_in_scope: Default::default(),
         all_trait_impls: Default::default(),
         all_macro_rules: Default::default(),
+        extern_doc_reachable: Default::default(),
+        local_doc_reachable: Default::default(),
         document_private_items,
     };
 
@@ -61,6 +64,7 @@ pub(crate) fn early_resolve_intra_doc_links(
         traits_in_scope: link_resolver.traits_in_scope,
         all_trait_impls: Some(link_resolver.all_trait_impls),
         all_macro_rules: link_resolver.all_macro_rules,
+        extern_doc_reachable: link_resolver.extern_doc_reachable,
     }
 }
 
@@ -77,6 +81,15 @@ struct EarlyDocLinkResolver<'r, 'ra> {
     traits_in_scope: DefIdMap<Vec<TraitCandidate>>,
     all_trait_impls: Vec<DefId>,
     all_macro_rules: FxHashMap<Symbol, Res<ast::NodeId>>,
+    /// This set is used as a seed for `effective_visibilities`, which are then extended by some
+    /// more items using `lib_embargo_visit_item` during doc inlining.
+    extern_doc_reachable: DefIdSet,
+    /// This is an easily identifiable superset of items added to `effective_visibilities`
+    /// using `lib_embargo_visit_item` during doc inlining.
+    /// The union of `(extern,local)_doc_reachable` is therefore a superset of
+    /// `effective_visibilities` and can be used for pruning extern impls here
+    /// in early doc link resolution.
+    local_doc_reachable: DefIdSet,
     document_private_items: bool,
 }
 
@@ -105,6 +118,10 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> {
         }
     }
 
+    fn is_doc_reachable(&self, def_id: DefId) -> bool {
+        self.extern_doc_reachable.contains(&def_id) || self.local_doc_reachable.contains(&def_id)
+    }
+
     /// Add traits in scope for links in impls collected by the `collect-intra-doc-links` pass.
     /// That pass filters impls using type-based information, but we don't yet have such
     /// information here, so we just conservatively calculate traits in scope for *all* modules
@@ -114,6 +131,14 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> {
         let mut start_cnum = 0;
         loop {
             let crates = Vec::from_iter(self.resolver.cstore().crates_untracked());
+            for cnum in &crates[start_cnum..] {
+                early_lib_embargo_visit_item(
+                    self.resolver,
+                    &mut self.extern_doc_reachable,
+                    cnum.as_def_id(),
+                    true,
+                );
+            }
             for &cnum in &crates[start_cnum..] {
                 let all_trait_impls =
                     Vec::from_iter(self.resolver.cstore().trait_impls_in_crate_untracked(cnum));
@@ -127,28 +152,26 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> {
                 // privacy, private traits and impls from other crates are never documented in
                 // the current crate, and links in their doc comments are not resolved.
                 for &(trait_def_id, impl_def_id, simplified_self_ty) in &all_trait_impls {
-                    if self.resolver.cstore().visibility_untracked(trait_def_id).is_public()
-                        && simplified_self_ty.and_then(|ty| ty.def()).map_or(true, |ty_def_id| {
-                            self.resolver.cstore().visibility_untracked(ty_def_id).is_public()
-                        })
+                    if self.is_doc_reachable(trait_def_id)
+                        && simplified_self_ty
+                            .and_then(|ty| ty.def())
+                            .map_or(true, |ty_def_id| self.is_doc_reachable(ty_def_id))
                     {
                         if self.visited_mods.insert(trait_def_id) {
                             self.resolve_doc_links_extern_impl(trait_def_id, false);
                         }
                         self.resolve_doc_links_extern_impl(impl_def_id, false);
                     }
+                    self.all_trait_impls.push(impl_def_id);
                 }
                 for (ty_def_id, impl_def_id) in all_inherent_impls {
-                    if self.resolver.cstore().visibility_untracked(ty_def_id).is_public() {
+                    if self.is_doc_reachable(ty_def_id) {
                         self.resolve_doc_links_extern_impl(impl_def_id, true);
                     }
                 }
                 for impl_def_id in all_incoherent_impls {
                     self.resolve_doc_links_extern_impl(impl_def_id, true);
                 }
-
-                self.all_trait_impls
-                    .extend(all_trait_impls.into_iter().map(|(_, def_id, _)| def_id));
             }
 
             if crates.len() > start_cnum {
@@ -298,6 +321,7 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> {
                     && module_id.is_local()
             {
                 if let Some(def_id) = child.res.opt_def_id() && !def_id.is_local() {
+                    self.local_doc_reachable.insert(def_id);
                     let scope_id = match child.res {
                         Res::Def(
                             DefKind::Variant
diff --git a/src/librustdoc/visit_lib.rs b/src/librustdoc/visit_lib.rs
index fd4f9254107ca..07d8b78d767db 100644
--- a/src/librustdoc/visit_lib.rs
+++ b/src/librustdoc/visit_lib.rs
@@ -1,7 +1,8 @@
 use crate::core::DocContext;
-use rustc_hir::def::DefKind;
+use rustc_hir::def::{DefKind, Res};
 use rustc_hir::def_id::{DefId, DefIdSet};
 use rustc_middle::ty::TyCtxt;
+use rustc_resolve::Resolver;
 
 // FIXME: this may not be exhaustive, but is sufficient for rustdocs current uses
 
@@ -25,6 +26,10 @@ impl RustdocEffectiveVisibilities {
     define_method!(is_directly_public);
     define_method!(is_exported);
     define_method!(is_reachable);
+
+    pub(crate) fn init(&mut self, extern_public: DefIdSet) {
+        self.extern_public = extern_public;
+    }
 }
 
 pub(crate) fn lib_embargo_visit_item(cx: &mut DocContext<'_>, def_id: DefId) {
@@ -37,6 +42,17 @@ pub(crate) fn lib_embargo_visit_item(cx: &mut DocContext<'_>, def_id: DefId) {
     .visit_item(def_id)
 }
 
+pub(crate) fn early_lib_embargo_visit_item(
+    resolver: &Resolver<'_>,
+    extern_public: &mut DefIdSet,
+    def_id: DefId,
+    is_mod: bool,
+) {
+    assert!(!def_id.is_local());
+    EarlyLibEmbargoVisitor { resolver, extern_public, visited_mods: Default::default() }
+        .visit_item(def_id, is_mod)
+}
+
 /// Similar to `librustc_privacy::EmbargoVisitor`, but also takes
 /// specific rustdoc annotations into account (i.e., `doc(hidden)`)
 struct LibEmbargoVisitor<'a, 'tcx> {
@@ -47,6 +63,14 @@ struct LibEmbargoVisitor<'a, 'tcx> {
     visited_mods: DefIdSet,
 }
 
+struct EarlyLibEmbargoVisitor<'r, 'ra> {
+    resolver: &'r Resolver<'ra>,
+    // Effective visibilities for reachable nodes
+    extern_public: &'r mut DefIdSet,
+    // Keeps track of already visited modules, in case a module re-exports its parent
+    visited_mods: DefIdSet,
+}
+
 impl LibEmbargoVisitor<'_, '_> {
     fn visit_mod(&mut self, def_id: DefId) {
         if !self.visited_mods.insert(def_id) {
@@ -71,3 +95,28 @@ impl LibEmbargoVisitor<'_, '_> {
         }
     }
 }
+
+impl EarlyLibEmbargoVisitor<'_, '_> {
+    fn visit_mod(&mut self, def_id: DefId) {
+        if !self.visited_mods.insert(def_id) {
+            return;
+        }
+
+        for item in self.resolver.cstore().module_children_untracked(def_id, self.resolver.sess()) {
+            if let Some(def_id) = item.res.opt_def_id() {
+                if item.vis.is_public() {
+                    self.visit_item(def_id, matches!(item.res, Res::Def(DefKind::Mod, _)));
+                }
+            }
+        }
+    }
+
+    fn visit_item(&mut self, def_id: DefId, is_mod: bool) {
+        if !self.resolver.cstore().is_doc_hidden_untracked(def_id) {
+            self.extern_public.insert(def_id);
+            if is_mod {
+                self.visit_mod(def_id);
+            }
+        }
+    }
+}