diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 5463397f475a599b9192322ef8ad2dcf3784094d..f91a56d1cc6a2b64f4ae0a7cbbcbd261dea67b31 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -17,6 +17,7 @@
 #= require jquery.atwho
 #= require jquery.scrollTo
 #= require jquery.turbolinks
+#= require jquery.stickytabs
 #= require d3
 #= require cal-heatmap
 #= require turbolinks
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 6055b60608622e8413d5e72874ec109da3f825a4..6f7021c43fd1df88481b988c68aabbdd8b6e3cb5 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,13 +3,6 @@ class UsersController < ApplicationController
   before_action :set_user
 
   def show
-    @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
-    
-    @projects = PersonalProjectsFinder.new(@user).execute(current_user)
-    @projects = @projects.page(params[:page]).per(PER_PAGE)
-
-    @groups = @user.groups.order_id_desc
-
     respond_to do |format|
       format.html
 
@@ -25,6 +18,24 @@ class UsersController < ApplicationController
     end
   end
 
+  def groups
+    load_groups
+
+    render 'shared/groups/_list', locals: { groups: @groups }, layout: false
+  end
+
+  def user_projects
+    load_projects
+
+    render 'shared/projects/_list', locals: { projects: @projects, remote: true }, layout: false
+  end
+
+  def user_contributed_projects
+    load_contributed_projects
+
+    render 'shared/projects/_list', locals: { projects: @contributed_projects }, layout: false
+  end
+
   def calendar
     calendar = contributions_calendar
     @timestamps = calendar.timestamps
@@ -69,6 +80,20 @@ class UsersController < ApplicationController
       limit_recent(20, params[:offset])
   end
 
+  def load_projects
+    @projects =
+      PersonalProjectsFinder.new(@user).execute(current_user)
+      .page(params[:page]).per(PER_PAGE)
+  end
+
+  def load_contributed_projects
+    @contributed_projects = contributed_projects.joined(@user)
+  end
+
+  def load_groups
+    @groups = @user.groups.order_id_desc
+  end
+
   def projects_for_current_user
     ProjectsFinder.new.execute(current_user)
   end
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 6148d8cb3d2fc7ebcdf93a6e2f3eda84e5ba23e9..76c7d5ee2e137afb666dce5a78cca99c31e41f2a 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -55,3 +55,6 @@
 - else
   %p.nav-links.no-top
     No projects to show
+
+:javascript
+  $('.nav-links').stickyTabs();
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..0833c6e61e5295bb4c4f6b45065c8a2a96d8410b
--- /dev/null
+++ b/app/views/shared/groups/_list.html.haml
@@ -0,0 +1,3 @@
+- if groups.any?
+  - groups.each_with_index do |group, i|
+    = render "shared/groups/group", group: group
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index e75af50a53765a9cdb60150bf5ab930f81af2931..b5c0c7ed57c2cec2b7df7b1be2bb376a0732e995 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -6,6 +6,7 @@
 - ci = false unless local_assigns[:ci] == true
 - skip_namespace = false unless local_assigns[:skip_namespace] == true
 - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
+- remote = false unless local_assigns[:remote] == true
 
 %ul.projects-list.content-list
   - if projects.any?
@@ -21,7 +22,7 @@
           #{projects_limit} of #{pluralize(projects.count, 'project')} displayed.
           = link_to '#', class: 'js-expand' do
             Show all
-    = paginate projects, theme: "gitlab" if projects.respond_to? :total_pages
+    = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
   - else
     %h3 No projects found
 
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index d109635fa1edbe7af267f13f9b3aeaf81db9b45d..33530ddd797df370610d2306e1bf7972fb07b323 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -39,7 +39,7 @@
       @#{@user.username}
     %span.middle-dot-divider
       Member since #{@user.created_at.to_s(:medium)}
-  
+
   - if @user.bio.present?
     .cover-desc
       %p.profile-user-bio
@@ -73,18 +73,15 @@
     %li.active
       = link_to "#activity", 'data-toggle' => 'tab' do
         Activity
-    - if @groups.any?
-      %li
-        = link_to "#groups", 'data-toggle' => 'tab' do
-          Groups
-    - if @contributed_projects.present?
-      %li
-        = link_to "#contributed", 'data-toggle' => 'tab' do
-          Contributed projects
-    - if @projects.present?
-      %li
-        = link_to "#personal", 'data-toggle' => 'tab' do
-          Personal projects
+    %li
+      = link_to "#groups", 'data-toggle' => 'tab' do
+        Groups
+    %li
+      = link_to "#contributed", 'data-toggle' => 'tab' do
+        Contributed projects
+    %li
+      = link_to "#personal", 'data-toggle' => 'tab' do
+        Personal projects
 
 %div{ class: container_class }
   .tab-content
@@ -100,25 +97,28 @@
       .content_list
       = spinner
 
-    - if @groups.any?
-      .tab-pane#groups
-        %ul.content-list
-          - @groups.each do |group|
-            = render 'shared/groups/group', group: group
-
-    - if @contributed_projects.present?
-      .tab-pane#contributed
-        .contributed-projects
-          = render 'shared/projects/list',
-            projects: @contributed_projects.sort_by(&:star_count).reverse,
-            projects_limit: 10, stars: true, avatar: true
-
-    - if @projects.present?
-      .tab-pane#personal
-        .personal-projects
-          = render 'shared/projects/list',
-            projects: @projects.sort_by(&:star_count).reverse,
-            projects_limit: 10, stars: true, avatar: true
+    .tab-pane#groups
+      %ul.content-list.user-groups
+        %h4.center.light
+          %i.fa.fa-spinner.fa-spin
+
+    .tab-pane#contributed
+      .contributed-projects
+        %h4.center.light
+          %i.fa.fa-spinner.fa-spin
+
+    .tab-pane#personal
+      .personal-projects
+        %h4.center.light
+          %i.fa.fa-spinner.fa-spin
 
 :javascript
+  $('.nav-links').stickyTabs();
   $(".user-calendar").load("#{user_calendar_path}");
+  $(".user-groups").load("#{user_groups_path}");
+  $(".contributed-projects").load("#{user_contributed_projects_path}");
+  $(".personal-projects").load("#{user_projects_path}");
+
+  $("body").on("ajax:success", function(e, data, status, xhr) {
+    $(".personal-projects").html(xhr.responseText)
+  });
diff --git a/config/routes.rb b/config/routes.rb
index a2acf170a6b2b7fbe8634824b6fc0eab76b272cf..e1448d231b5f89f6a065bf898e41995ccb26cea8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -332,6 +332,15 @@ Rails.application.routes.draw do
   get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities,
       constraints: { username: /.*/ }
 
+  get 'u/:username/groups' => 'users#groups', as: :user_groups,
+      constraints: { username: /.*/ }
+
+  get 'u/:username/projects' => 'users#user_projects', as: :user_projects,
+      constraints: { username: /.*/ }
+
+  get 'u/:username/contributed_projects' => 'users#user_contributed_projects', as: :user_contributed_projects,
+      constraints: { username: /.*/ }
+
   get '/u/:username' => 'users#show', as: :user,
       constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
 
diff --git a/features/user.feature b/features/user.feature
index 35eae842e77184cc231bacfbf1d6c3ae69c4bb69..209afab4db78b9db2cfc44b10d84f52f91d3e8b4 100644
--- a/features/user.feature
+++ b/features/user.feature
@@ -5,6 +5,7 @@ Feature: User
 
   # Signed out
 
+  @javascript
   Scenario: I visit user "John Doe" page while not signed in when he owns a public project
     Given "John Doe" owns internal project "Internal"
     And "John Doe" owns public project "Community"
@@ -16,6 +17,7 @@ Feature: User
 
   # Signed in as someone else
 
+  @javascript
   Scenario: I visit user "John Doe" page while signed in as someone else when he owns a public project
     Given "John Doe" owns public project "Community"
     And "John Doe" owns internal project "Internal"
@@ -26,6 +28,7 @@ Feature: User
     And I should see project "Internal"
     And I should see project "Community"
 
+  @javascript
   Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a public project
     Given "John Doe" owns internal project "Internal"
     And I sign in as a user
@@ -35,6 +38,7 @@ Feature: User
     And I should see project "Internal"
     And I should not see project "Community"
 
+  @javascript
   Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a project I can see
     Given I sign in as a user
     When I visit user "John Doe" page
@@ -45,6 +49,7 @@ Feature: User
 
   # Signed in as the user himself
 
+  @javascript
   Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has a public project
     Given "John Doe" owns internal project "Internal"
     And "John Doe" owns public project "Community"
@@ -55,6 +60,7 @@ Feature: User
     And I should see project "Internal"
     And I should see project "Community"
 
+  @javascript
   Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has no public project
     Given I sign in as "John Doe"
     When I visit user "John Doe" page
diff --git a/vendor/assets/javascripts/jquery.stickytabs.js b/vendor/assets/javascripts/jquery.stickytabs.js
new file mode 100644
index 0000000000000000000000000000000000000000..8856fb04262309acfb45f8e4a419160d1336f7e9
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.stickytabs.js
@@ -0,0 +1,61 @@
+/**
+ * jQuery Plugin: Sticky Tabs
+ *
+ * @author Aidan Lister <aidan@php.net>
+ * @version 1.2.0
+ */
+(function ( $ ) {
+    $.fn.stickyTabs = function( options ) {
+        var context = this
+
+        var settings = $.extend({
+            getHashCallback: function(hash, btn) { return hash },
+            selectorAttribute: "href",
+            backToTop: false,
+            initialTab: $('li.active > a', context)
+        }, options );
+
+        // Show the tab corresponding with the hash in the URL, or the first tab.
+        var showTabFromHash = function() {
+          var hash = settings.selectorAttribute == "href" ? window.location.hash : window.location.hash.substring(1);
+          var selector = hash ? 'a[' + settings.selectorAttribute +'="' + hash + '"]' : settings.initialTab;
+          $(selector, context).tab('show');
+          setTimeout(backToTop, 1);
+        }
+
+        // We use pushState if it's available so the page won't jump, otherwise a shim.
+        var changeHash = function(hash) {
+          if (history && history.pushState) {
+            history.pushState(null, null, window.location.pathname + window.location.search + '#' + hash);
+          } else {
+            scrollV = document.body.scrollTop;
+            scrollH = document.body.scrollLeft;
+            window.location.hash = hash;
+            document.body.scrollTop = scrollV;
+            document.body.scrollLeft = scrollH;
+          }
+        }
+
+        var backToTop = function() {
+          if (settings.backToTop === true) {
+            window.scrollTo(0, 0);
+          }
+        }
+
+        // Set the correct tab when the page loads
+        showTabFromHash();
+
+        // Set the correct tab when a user uses their back/forward button
+        $(window).on('hashchange', showTabFromHash);
+
+        // Change the URL when tabs are clicked
+        $('a', context).on('click', function(e) {
+          var hash = this.href.split('#')[1];
+          var adjustedhash = settings.getHashCallback(hash, this);
+          changeHash(adjustedhash);
+          setTimeout(backToTop, 1);
+        });
+
+        return this;
+    };
+}( jQuery ));